Explore CloudLang project structure (@explore subagent)

/home/jm/data/ai/cloudlang-trees/react · 7 messages · 66 parts · · 115,888 tokens
Created Feb 10, 02:29:32 AM · Updated Feb 11, 04:54:46 AM · 1 syncs
userFeb 10, 02:29:32 AM

Thoroughly explore the project at /home/jm/data/ai/cloudlang-trees/react. I need to understand:

  1. The full directory tree structure
  2. The dune-project and opam files for dependencies
  3. The current compiler pipeline - what phases exist, what IR types are defined
  4. What AST/IR types are already defined
  5. Any existing analysis passes

Read key files: dune-project, any .ml and .mli files in lib/, bin/, and test/. Give me a comprehensive picture of where this project is and what's already built. Return file paths and line numbers for key definitions.

assistantFeb 10, 02:29:32 AM
claude-opus-4.6 · github-copilot

I'll systematically explore the project. Let me start with the directory structure and key configuration files, then dive into the source code.

bashcompleted
Full directory tree listing
Output
/home/jm/data/ai/cloudlang-trees/react/.git
/home/jm/data/ai/cloudlang-trees/react/.claude/context/capnweb-integration.md
/home/jm/data/ai/cloudlang-trees/react/.github/workflows/ci.yml
/home/jm/data/ai/cloudlang-trees/react/.gitignore
/home/jm/data/ai/cloudlang-trees/react/.ocamlformat
/home/jm/data/ai/cloudlang-trees/react/.opencode/context/cloudlang-architecture-design.md
/home/jm/data/ai/cloudlang-trees/react/.opencode/context/typescript-ast-json-research.md
/home/jm/data/ai/cloudlang-trees/react/.opencode/context/typescript-ast-summary.md
/home/jm/data/ai/cloudlang-trees/react/.opencode/package.json
/home/jm/data/ai/cloudlang-trees/react/.opencode/.gitignore
/home/jm/data/ai/cloudlang-trees/react/AGENTS.md
/home/jm/data/ai/cloudlang-trees/react/LICENSE.md
/home/jm/data/ai/cloudlang-trees/react/README.md
/home/jm/data/ai/cloudlang-trees/react/bin/dune
/home/jm/data/ai/cloudlang-trees/react/bin/main.ml
/home/jm/data/ai/cloudlang-trees/react/cloudlang.opam
/home/jm/data/ai/cloudlang-trees/react/docs/durable_objects_ir_design.md
/home/jm/data/ai/cloudlang-trees/react/dune-project
/home/jm/data/ai/cloudlang-trees/react/examples/closure_worker.cl
/home/jm/data/ai/cloudlang-trees/react/examples/hello.cl
/home/jm/data/ai/cloudlang-trees/react/examples/kv-store.ts
/home/jm/data/ai/cloudlang-trees/react/examples/task-api-server.ts
/home/jm/data/ai/cloudlang-trees/react/examples/task-api.ts
/home/jm/data/ai/cloudlang-trees/react/lib/ast.ml
/home/jm/data/ai/cloudlang-trees/react/lib/ast.mli
/home/jm/data/ai/cloudlang-trees/react/lib/cloudlang.ml
/home/jm/data/ai/cloudlang-trees/react/lib/cloudlang.mli
/home/jm/data/ai/cloudlang-trees/react/lib/decompose.ml
/home/jm/data/ai/cloudlang-trees/react/lib/decompose.mli
/home/jm/data/ai/cloudlang-trees/react/lib/dune
/home/jm/data/ai/cloudlang-trees/react/lib/emit.ml
/home/jm/data/ai/cloudlang-trees/react/lib/emit.mli
/home/jm/data/ai/cloudlang-trees/react/lib/ir.ml
/home/jm/data/ai/cloudlang-trees/react/lib/ir.mli
/home/jm/data/a...
globcompleted
Output
/home/jm/data/ai/cloudlang-trees/react/.opencode/.gitignore
/home/jm/data/ai/cloudlang-trees/react/.opencode/package.json
/home/jm/data/ai/cloudlang-trees/react/verify/verify.ts
/home/jm/data/ai/cloudlang-trees/react/verify/scenarios.ts
/home/jm/data/ai/cloudlang-trees/react/verify/package.json
/home/jm/data/ai/cloudlang-trees/react/tools/ts-to-ast.ts
/home/jm/data/ai/cloudlang-trees/react/tools/package.json
/home/jm/data/ai/cloudlang-trees/react/test/test_task_api.ml
/home/jm/data/ai/cloudlang-trees/react/test/test_scope.ml
/home/jm/data/ai/cloudlang-trees/react/test/test_parse.ml
/home/jm/data/ai/cloudlang-trees/react/test/test_lower.ml
/home/jm/data/ai/cloudlang-trees/react/test/test_ir.ml
/home/jm/data/ai/cloudlang-trees/react/test/test_generate.ml
/home/jm/data/ai/cloudlang-trees/react/test/test_emit.ml
/home/jm/data/ai/cloudlang-trees/react/test/test_durable_object_hints.ml
/home/jm/data/ai/cloudlang-trees/react/test/test_decompose.ml
/home/jm/data/ai/cloudlang-trees/react/test/test_cloudlang.ml
/home/jm/data/ai/cloudlang-trees/react/test/fixture.ml
/home/jm/data/ai/cloudlang-trees/react/test/dune
/home/jm/data/ai/cloudlang-trees/react/lib/wrangler.mli
/home/jm/data/ai/cloudlang-trees/react/lib/wrangler.ml
/home/jm/data/ai/cloudlang-trees/react/lib/subprocess.mli
/home/jm/data/ai/cloudlang-trees/react/lib/subprocess.ml
/home/jm/data/ai/cloudlang-trees/react/lib/string_list.mli
/home/jm/data/ai/cloudlang-trees/react/lib/string_list.ml
/home/jm/data/ai/cloudlang-trees/react/lib/scope.mli
/home/jm/data/ai/cloudlang-trees/react/lib/scope.ml
/home/jm/data/ai/cloudlang-trees/react/lib/parse.mli
/home/jm/data/ai/cloudlang-trees/react/lib/parse.ml
/home/jm/data/ai/cloudlang-trees/react/lib/lower.mli
/home/jm/data/ai/cloudlang-trees/react/lib/lower.ml
/home/jm/data/ai/cloudlang-trees/react/lib/ir.mli
/home/jm/data/ai/cloudlang-trees/react/lib/ir.ml
/home/jm/data/ai/cloudlang-trees/react/lib/emit.mli
/home/jm/data/ai/cloudlang-trees/react/lib/emit.ml
/home/jm/data/ai/cl...
readcompleted
dune-project
Output
<file>
00001| (lang dune 3.20)
00002| (authors "Jason Manning <manno@j9m>")
00003| (maintainers "Jason Manning <manno@j9m>")
00004| (license GPL-3.0-or-later)
00005| (source (github manno23/cloudlang))
00006| 
00007| (name cloudlang)
00008| (generate_opam_files true)
00009| (maintenance_intent "(latest)")
00010| (package
00011|  (name cloudlang)
00012|  (synopsis "TypeScript to Cloudflare Worker decomposition compiler")
00013|  (depends
00014|   ocaml
00015|   jsont
00016|   bytesrw))
00017| 

(End of file - total 17 lines)
</file>
10,674in / 248out
assistantFeb 10, 02:29:36 AM
claude-opus-4.6 · github-copilot

Now let me read all the core library files and build files:

readcompleted
cloudlang.opam
Output
<file>
00001| # This file is generated by dune, edit dune-project instead
00002| opam-version: "2.0"
00003| synopsis: "TypeScript to Cloudflare Worker decomposition compiler"
00004| maintainer: ["Jason Manning <manno@j9m>"]
00005| authors: ["Jason Manning <manno@j9m>"]
00006| license: "GPL-3.0-or-later"
00007| homepage: "https://github.com/manno23/cloudlang"
00008| bug-reports: "https://github.com/manno23/cloudlang/issues"
00009| depends: [
00010|   "dune" {>= "3.20"}
00011|   "ocaml"
00012|   "jsont"
00013|   "bytesrw"
00014|   "odoc" {with-doc}
00015| ]
00016| build: [
00017|   ["dune" "subst"] {dev}
00018|   [
00019|     "dune"
00020|     "build"
00021|     "-p"
00022|     name
00023|     "-j"
00024|     jobs
00025|     "@install"
00026|     "@runtest" {with-test}
00027|     "@doc" {with-doc}
00028|   ]
00029| ]
00030| dev-repo: "git+https://github.com/manno23/cloudlang.git"
00031| x-maintenance-intent: ["(latest)"]
00032| 

(End of file - total 32 lines)
</file>
readcompleted
lib/dune
Output
<file>
00001| (library
00002|  (name cloudlang)
00003|  (libraries jsont jsont.bytesrw unix))
00004| 

(End of file - total 4 lines)
</file>
readcompleted
bin/dune
Output
<file>
00001| (executable
00002|  (public_name cloudlang)
00003|  (name main)
00004|  (libraries cloudlang unix))
00005| 

(End of file - total 5 lines)
</file>
readcompleted
test/dune
Output
<file>
00001| (test
00002|  (name test_cloudlang)
00003|  (libraries cloudlang))
00004| 

(End of file - total 4 lines)
</file>
readcompleted
lib/ast.ml
Output
<file>
00001| (** ESTree-compatible AST subset for TypeScript.
00002| 
00003|     This represents the minimal set of TypeScript constructs needed for
00004|     closure analysis and Worker decomposition. Node types follow the ESTree
00005|     specification used by @typescript-eslint/typescript-estree. *)
00006| 
00007| type var_kind = Const | Let | Var
00008| 
00009| type literal_value =
00010|   | LString of string
00011|   | LNumber of float
00012|   | LBool of bool
00013|   | LNull
00014|   | LUndefined
00015| 
00016| (** A single AST node. The variant name matches the ESTree "type" field. *)
00017| type node =
00018|   | Program of { body : node list }
00019|   | VariableDeclaration of { kind : var_kind; declarations : node list }
00020|   | VariableDeclarator of { id : node; init : node option }
00021|   | Identifier of { name : string }
00022|   | Literal of { value : literal_value }
00023|   | ArrowFunctionExpression of {
00024|       params : node list;
00025|       body : node;
00026|       async_ : bool;
00027|     }
00028|   | BlockStatement of { body : node list }
00029|   | ReturnStatement of { argument : node option }
00030|   | IfStatement of { test : node; consequent : node; alternate : node option }
00031|   | ExpressionStatement of { expression : node }
00032|   | CallExpression of { callee : node; arguments : node list }
00033|   | MemberExpression of { object_ : node; property : node }
00034|   | NewExpression of { callee : node; arguments : node list }
00035|   | BinaryExpression of { operator : string; left : node; right : node }
00036|   | LogicalExpression of { operator : string; left : node; right : node }
00037|   | AwaitExpression of { argument : node }
00038|   | RawCode of { code : string }
00039|       (** Verbatim TypeScript fragment. Used by the lowering pass for
00040|           constructs that don't have AST representation (e.g. object
00041|           literals for fetch options). Not produced by the parser. *)
00042|   | ExportNam...
readcompleted
lib/ast.mli
Output
<file>
00001| (** ESTree-compatible AST subset for TypeScript. *)
00002| 
00003| type var_kind = Const | Let | Var
00004| 
00005| type literal_value =
00006|   | LString of string
00007|   | LNumber of float
00008|   | LBool of bool
00009|   | LNull
00010|   | LUndefined
00011| 
00012| type node =
00013|   | Program of { body : node list }
00014|   | VariableDeclaration of { kind : var_kind; declarations : node list }
00015|   | VariableDeclarator of { id : node; init : node option }
00016|   | Identifier of { name : string }
00017|   | Literal of { value : literal_value }
00018|   | ArrowFunctionExpression of {
00019|       params : node list;
00020|       body : node;
00021|       async_ : bool;
00022|     }
00023|   | BlockStatement of { body : node list }
00024|   | ReturnStatement of { argument : node option }
00025|   | IfStatement of { test : node; consequent : node; alternate : node option }
00026|   | ExpressionStatement of { expression : node }
00027|   | CallExpression of { callee : node; arguments : node list }
00028|   | MemberExpression of { object_ : node; property : node }
00029|   | NewExpression of { callee : node; arguments : node list }
00030|   | BinaryExpression of { operator : string; left : node; right : node }
00031|   | LogicalExpression of { operator : string; left : node; right : node }
00032|   | AwaitExpression of { argument : node }
00033|   | RawCode of { code : string }
00034|   | ExportNamedDeclaration of { specifiers : node list }
00035|   | ExportSpecifier of { local : node }
00036| 

(End of file - total 36 lines)
</file>
readcompleted
lib/ir.ml
Output
<file>
00001| type binding =
00002|   | D1Binding of { name : string; database_id : string }
00003|   | KVBinding of { name : string; namespace_id : string }
00004|   | R2Binding of { name : string; bucket_name : string }
00005|   | ServiceBinding of { name : string; service : string }
00006| 
00007| type do_marker =
00008|   | SerialConflictHotspot
00009|   | GlobalSingletonNeeded
00010|   | EntityAffinityKeyed of { key_hint : string option }
00011|   | StrictOrderingRequired
00012|   | ConsistencyBoundaryCrossed
00013|   | RealtimeFanout
00014|   | DurableTimerNeeded
00015|   | HighFanInCoordinator
00016|   | StatelessFastPathPreferWorker
00017| 
00018| type coordination_intent =
00019|   | PerEntitySerial of { key_expr : string; reason : string }
00020|   | GlobalSingleton of { name : string; reason : string }
00021|   | RealtimeRoom of { room_key_expr : string }
00022|   | ScheduledQueue of { queue_key_expr : string }
00023| 
00024| type routing_policy =
00025|   | DeterministicName of { key_expr : string }
00026|   | ShardedName of { key_expr : string; shards : int }
00027|   | UniqueIdPool of { pool : string }
00028| 
00029| type storage_backend = DoSqlite | DoSyncKv
00030| type alarm_mode = NoAlarm | SingleAlarm | QueuedAlarm
00031| 
00032| type durability_policy = {
00033|   storage_backend : storage_backend;
00034|   alarm_mode : alarm_mode;
00035|   pitr_required : bool;
00036| }
00037| 
00038| type do_annotation = {
00039|   class_name : string;
00040|   binding_name : string;
00041|   markers : do_marker list;
00042|   intent : coordination_intent;
00043|   routing : routing_policy;
00044|   durability : durability_policy;
00045|   do_fit_score : int;
00046|   reasons : string list;
00047| }
00048| 
00049| type worker = {
00050|   name : string;
00051|   script : string;
00052|   bindings : binding list;
00053|   routes : string list;
00054|   do_annotation : do_annotation option;
00055| }
00056| 
00057| type durable_object = { class_name : string; scrip...
readcompleted
lib/ir.mli
Output
<file>
00001| type binding =
00002|   | D1Binding of { name : string; database_id : string }
00003|   | KVBinding of { name : string; namespace_id : string }
00004|   | R2Binding of { name : string; bucket_name : string }
00005|   | ServiceBinding of { name : string; service : string }
00006| 
00007| type do_marker =
00008|   | SerialConflictHotspot
00009|   | GlobalSingletonNeeded
00010|   | EntityAffinityKeyed of { key_hint : string option }
00011|   | StrictOrderingRequired
00012|   | ConsistencyBoundaryCrossed
00013|   | RealtimeFanout
00014|   | DurableTimerNeeded
00015|   | HighFanInCoordinator
00016|   | StatelessFastPathPreferWorker
00017| 
00018| type coordination_intent =
00019|   | PerEntitySerial of { key_expr : string; reason : string }
00020|   | GlobalSingleton of { name : string; reason : string }
00021|   | RealtimeRoom of { room_key_expr : string }
00022|   | ScheduledQueue of { queue_key_expr : string }
00023| 
00024| type routing_policy =
00025|   | DeterministicName of { key_expr : string }
00026|   | ShardedName of { key_expr : string; shards : int }
00027|   | UniqueIdPool of { pool : string }
00028| 
00029| type storage_backend = DoSqlite | DoSyncKv
00030| type alarm_mode = NoAlarm | SingleAlarm | QueuedAlarm
00031| 
00032| type durability_policy = {
00033|   storage_backend : storage_backend;
00034|   alarm_mode : alarm_mode;
00035|   pitr_required : bool;
00036| }
00037| 
00038| type do_annotation = {
00039|   class_name : string;
00040|   binding_name : string;
00041|   markers : do_marker list;
00042|   intent : coordination_intent;
00043|   routing : routing_policy;
00044|   durability : durability_policy;
00045|   do_fit_score : int;
00046|   reasons : string list;
00047| }
00048| 
00049| type worker = {
00050|   name : string;
00051|   script : string;
00052|   bindings : binding list;
00053|   routes : string list;
00054|   do_annotation : do_annotation option;
00055| }
00056| 
00057| type durable_object = { class_name : string; scrip...
readcompleted
lib/parse.ml
Output
<file>
00001| let var_kind_jsont =
00002|   Jsont.enum ~kind:"var_kind"
00003|     [ ("const", Ast.Const); ("let", Ast.Let); ("var", Ast.Var) ]
00004| 
00005| let literal_value_jsont : Ast.literal_value Jsont.t =
00006|   let dec_string =
00007|     Jsont.map Jsont.string ~kind:"LString" ~dec:(fun s -> Ast.LString s)
00008|   in
00009|   let dec_number =
00010|     Jsont.map Jsont.number ~kind:"LNumber" ~dec:(fun n -> Ast.LNumber n)
00011|   in
00012|   let dec_bool =
00013|     Jsont.map Jsont.bool ~kind:"LBool" ~dec:(fun b -> Ast.LBool b)
00014|   in
00015|   let dec_null = Jsont.null Ast.LNull in
00016|   Jsont.any ~kind:"literal_value" ~dec_string ~dec_number ~dec_bool ~dec_null ()
00017| 
00018| let node_jsont : Ast.node Jsont.t =
00019|   let rec t =
00020|     lazy
00021|       (let node = Jsont.rec' t in
00022|        let node_list = Jsont.list node in
00023| 
00024|        let program_obj =
00025|          Jsont.Object.map ~kind:"Program" (fun body -> Ast.Program { body })
00026|          |> Jsont.Object.mem "body" node_list
00027|          |> Jsont.Object.skip_unknown |> Jsont.Object.finish
00028|        in
00029| 
00030|        let variable_declaration_obj =
00031|          Jsont.Object.map ~kind:"VariableDeclaration" (fun kind declarations ->
00032|              Ast.VariableDeclaration { kind; declarations })
00033|          |> Jsont.Object.mem "kind" var_kind_jsont
00034|          |> Jsont.Object.mem "declarations" node_list
00035|          |> Jsont.Object.skip_unknown |> Jsont.Object.finish
00036|        in
00037| 
00038|        let variable_declarator_obj =
00039|          Jsont.Object.map ~kind:"VariableDeclarator" (fun id init ->
00040|              Ast.VariableDeclarator { id; init })
00041|          |> Jsont.Object.mem "id" node
00042|          |> Jsont.Object.opt_mem "init" node
00043|          |> Jsont.Object.skip_unknown |> Jsont.Object.finish
00044|        in
00045| 
00046|        let identifier_obj =
00047|          Jsont.Object.map ~kind:"Id...
readcompleted
lib/parse.mli
Output
<file>
00001| (** JSON → AST deserializer using jsont.
00002| 
00003|     Converts ESTree JSON (from @typescript-eslint/typescript-estree) into
00004|     CloudLang's Ast.node type. *)
00005| 
00006| val node_jsont : Ast.node Jsont.t
00007| (** The jsont codec for AST nodes. *)
00008| 
00009| val decode : string -> (Ast.node, string) result
00010| (** [decode json_string] parses a JSON string into an AST node.
00011|     Returns [Error msg] if parsing fails. *)
00012| 

(End of file - total 12 lines)
</file>
readcompleted
lib/scope.ml
Output
<file>
00001| (** Scope analysis for TypeScript ESTree AST.
00002| 
00003|     Two-pass analysis: 1. Declaration pass: collect all module-scope const/let
00004|     bindings, noting which are mutable state (new Map/Set) and which are
00005|     functions. 2. Reference pass: for each arrow function body, collect
00006|     Identifier references that are not locally declared -> these are free
00007|     variables. *)
00008| 
00009| type error = Expected_program_node
00010| 
00011| let error_to_string = function
00012|   | Expected_program_node -> "expected Program node"
00013| 
00014| type var_info = { name : string; is_mutable_state : bool; is_function : bool }
00015| 
00016| type closure_info = {
00017|   name : string;
00018|   free_vars : string list;
00019|   called_functions : string list;
00020|   captures_mutable : string list;
00021| }
00022| 
00023| type analysis_result = {
00024|   closures : closure_info list;
00025|   module_vars : var_info list;
00026|   exports : string list;
00027| }
00028| 
00029| (** Check whether an init expression is [new Map(...)], [new Set(...)], etc. *)
00030| let is_mutable_init = function
00031|   | Ast.NewExpression { callee = Ast.Identifier { name }; _ }
00032|     when name = "Map" || name = "Set" || name = "Array" ->
00033|       true
00034|   | _ -> false
00035| 
00036| (** Check whether an init expression is an arrow function. *)
00037| let is_arrow_init = function
00038|   | Ast.ArrowFunctionExpression _ -> true
00039|   | _ -> false
00040| 
00041| let option_exists predicate = function
00042|   | Some value -> predicate value
00043|   | None -> false
00044| 
00045| (** Extract parameter names from an arrow function's param list. *)
00046| let param_names params =
00047|   List.filter_map
00048|     (function Ast.Identifier { name } -> Some name | _ -> None)
00049|     params
00050| 
00051| (** Collect all identifiers referenced in an expression/statement, excluding
00052|     property names on the right side of member ...
readcompleted
lib/scope.mli
Output
<file>
00001| (** Scope analysis for TypeScript ESTree AST.
00002| 
00003|     Walks the AST to build a scope tree, identify variable declarations, and
00004|     compute free variables for each arrow function (closure). *)
00005| 
00006| (** Analysis errors. *)
00007| type error =
00008|   | Expected_program_node
00009|       (** The [analyze] entry point was called with a non-[Program] AST node. *)
00010| 
00011| val error_to_string : error -> string
00012| (** [error_to_string err] is a human-readable message for [err]. *)
00013| 
00014| type var_info = {
00015|   name : string;
00016|   is_mutable_state : bool;
00017|       (** [true] when the initialiser is [new Map()], [new Set()], etc. *)
00018|   is_function : bool;
00019|       (** [true] when the initialiser is an [ArrowFunctionExpression]. *)
00020| }
00021| (** Information about a declared variable. *)
00022| 
00023| type closure_info = {
00024|   name : string;  (** The [const] name this arrow function is assigned to. *)
00025|   free_vars : string list;  (** Variables referenced from outer scopes. *)
00026|   called_functions : string list;
00027|       (** Names of other module-level functions called in the body. *)
00028|   captures_mutable : string list;
00029|       (** Subset of [free_vars] that are mutable state variables. *)
00030| }
00031| (** Per-closure analysis result. *)
00032| 
00033| type analysis_result = {
00034|   closures : closure_info list;
00035|   module_vars : var_info list;  (** All module-scope variable declarations. *)
00036|   exports : string list;
00037|       (** Names appearing in [export { ... }] declarations. *)
00038| }
00039| (** Complete analysis result for a program. *)
00040| 
00041| val analyze : Ast.node -> (analysis_result, error) result
00042| 

(End of file - total 42 lines)
</file>
readcompleted
lib/lower.ml
Output
<file>
00001| (** Body lowering: rewrite function bodies for distributed execution.
00002| 
00003|     Transforms AST function bodies so that:
00004|     1. Map.get/set/delete/has on captured mutable state become KV API calls
00005|     2. Cross-group function calls become JSON-over-fetch to service bindings
00006|     3. All affected functions become async with env parameter
00007|     4. A fetch router dispatches incoming requests to handler functions *)
00008| 
00009| (** Rewriting context for a single function within a Worker group. *)
00010| type rewrite_ctx = {
00011|   owned_state : string list;
00012|       (** Mutable state variables owned by this group (Map -> KV). *)
00013|   service_deps : (string * string) list;
00014|       (** (function_name, target_group) pairs for cross-group calls. *)
00015| }
00016| 
00017| (** Rewrite a Map method call on a captured mutable state variable to a
00018|     KV API call.
00019| 
00020|     store.get(key)          -> await env.STORE.get(key)
00021|     store.set(key, value)   -> await env.STORE.put(key, value)
00022|     store.delete(key)       -> await env.STORE.delete(key)
00023|     store.has(key)          -> (await env.STORE.get(key)) !== null *)
00024| let rewrite_map_call (var_name : string) (method_name : string)
00025|     (args : Ast.node list) : Ast.node option =
00026|   let binding = String.uppercase_ascii var_name in
00027|   let env_member prop =
00028|     Ast.MemberExpression
00029|       {
00030|         object_ =
00031|           Ast.MemberExpression
00032|             {
00033|               object_ = Ast.Identifier { name = "env" };
00034|               property = Ast.Identifier { name = binding };
00035|             };
00036|         property = Ast.Identifier { name = prop };
00037|       }
00038|   in
00039|   let await_call prop arguments =
00040|     Ast.AwaitExpression
00041|       {
00042|         argument =
00043|           Ast.CallExpression { callee = env_member prop; arguments };
00044|   ...
readcompleted
lib/lower.mli
Output
<file>
00001| (** Body lowering: rewrite function bodies for distributed execution.
00002| 
00003|     Transforms AST function bodies so that:
00004|     1. Map.get/set/delete/has on captured mutable state become KV API calls
00005|     2. Cross-group function calls become JSON-over-fetch to service bindings
00006|     3. All affected functions become async with env parameter
00007|     4. A fetch router dispatches incoming requests to handler functions *)
00008| 
00009| type rewrite_ctx = {
00010|   owned_state : string list;
00011|   service_deps : (string * string) list;
00012| }
00013| 
00014| val rewrite_node : rewrite_ctx -> Ast.node -> Ast.node
00015| (** [rewrite_node ctx node] rewrites Map calls to KV calls and
00016|     cross-group function calls to service fetch calls. *)
00017| 
00018| val extract_functions :
00019|   Ast.node list -> (string * Ast.node list * Ast.node) list
00020| (** [extract_functions body] extracts arrow function declarations
00021|     from a program body as (name, params, body) triples. *)
00022| 
00023| val generate_worker_script :
00024|   Decompose.worker_group ->
00025|   Decompose.worker_group list ->
00026|   Scope.analysis_result ->
00027|   Ast.node list ->
00028|   string
00029| (** [generate_worker_script group all_groups analysis program_body]
00030|     generates a complete TypeScript Worker script for the given group,
00031|     with rewritten function bodies, Env interface, and fetch router. *)
00032| 

(End of file - total 32 lines)
</file>
readcompleted
lib/decompose.ml
Output
<file>
00001| (** Closure decomposition: scope analysis -> Worker groups -> IR.
00002| 
00003|     Algorithm: 1. Seed groups from mutable state: functions that capture the
00004|     same mutable variable are placed in the same group. 2. Remaining functions
00005|     (no mutable captures) become their own group. 3. Cross-group function calls
00006|     become service binding dependencies. 4. Convert groups to IR Workers with KV
00007|     and service bindings. *)
00008| 
00009| type worker_group = {
00010|   name : string;
00011|   functions : string list;
00012|   owned_state : string list;
00013|   service_deps : string list;
00014| }
00015| 
00016| (** Find which group a function belongs to. *)
00017| let find_group_of_function (groups : worker_group list) (fn_name : string) :
00018|     string option =
00019|   List.find_map
00020|     (fun g -> if List.mem fn_name g.functions then Some g.name else None)
00021|     groups
00022| 
00023| (** Derive a group name from the mutable state variable name. e.g. "store" ->
00024|     "storage", "cache" -> "cache" *)
00025| let group_name_of_state (state_var : string) : string =
00026|   if state_var = "store" then "storage" else state_var
00027| 
00028| let clamp_score (score : int) : int = max 0 (min 100 score)
00029| 
00030| let contains_substr (s : string) (sub : string) : bool =
00031|   let len_s = String.length s in
00032|   let len_sub = String.length sub in
00033|   if len_sub = 0 then true
00034|   else if len_sub > len_s then false
00035|   else
00036|     let rec loop i =
00037|       if i > len_s - len_sub then false
00038|       else if String.sub s i len_sub = sub then true
00039|       else loop (i + 1)
00040|     in
00041|     loop 0
00042| 
00043| let any_name_matches (names : string list) (keywords : string list) : bool =
00044|   List.exists
00045|     (fun name ->
00046|       let lowered = String.lowercase_ascii name in
00047|       List.exists (fun keyword -> contains_substr lowered keyword) keyword...
readcompleted
lib/decompose.mli
Output
<file>
00001| (** Closure decomposition: scope analysis results -> IR config.
00002| 
00003|     Groups closures into Workers based on shared mutable state, generates KV
00004|     bindings for mutable state and service bindings for cross-group function
00005|     calls. *)
00006| 
00007| type worker_group = {
00008|   name : string;
00009|   functions : string list;
00010|   owned_state : string list;
00011|   service_deps : string list;
00012| }
00013| (** A logical group of functions that will become a single Worker. *)
00014| 
00015| val decompose : Scope.analysis_result -> worker_group list
00016| 
00017| val find_group_of_function : worker_group list -> string -> string option
00018| (** [find_group_of_function groups fn_name] returns the name of the group
00019|     that contains [fn_name], or [None]. *)
00020| 
00021| val do_fit_score : worker_group -> Scope.analysis_result -> int
00022| (** [do_fit_score group analysis] is a heuristic score in [0, 100] indicating
00023|     how well [group] fits Durable Object coordination semantics. *)
00024| 
00025| val infer_do_annotation :
00026|   worker_group -> Scope.analysis_result -> Ir.do_annotation option
00027| (** [infer_do_annotation group analysis] derives a Durable Object hint for
00028|     [group] when the fit score is high enough. *)
00029| 
00030| val to_ir : worker_group list -> Scope.analysis_result -> Ir.config
00031| 

(End of file - total 31 lines)
</file>
readcompleted
lib/emit.ml
Output
<file>
00001| (** AST to TypeScript source code emitter.
00002| 
00003|     Converts ESTree AST nodes back to readable TypeScript source code.
00004|     Used by the body lowering pass to emit rewritten function bodies
00005|     into generated Worker scripts. *)
00006| 
00007| let indent_str (level : int) : string = String.make (level * 2) ' '
00008| 
00009| let format_float (f : float) : string =
00010|   if f = Float.round f then Printf.sprintf "%.0f" f
00011|   else Printf.sprintf "%g" f
00012| 
00013| (** Emit an expression node as inline TypeScript (no leading indent). *)
00014| let rec emit_expr (node : Ast.node) : string =
00015|   match node with
00016|   | Ast.Identifier { name } -> name
00017|   | Ast.Literal { value } -> (
00018|       match value with
00019|       | Ast.LString s -> Printf.sprintf {|"%s"|} s
00020|       | Ast.LNumber n -> format_float n
00021|       | Ast.LBool b -> string_of_bool b
00022|       | Ast.LNull -> "null"
00023|       | Ast.LUndefined -> "undefined")
00024|   | Ast.CallExpression { callee; arguments } ->
00025|       Printf.sprintf "%s(%s)" (emit_expr callee)
00026|         (String.concat ", " (List.map emit_expr arguments))
00027|   | Ast.MemberExpression { object_; property } ->
00028|       Printf.sprintf "%s.%s" (emit_expr object_) (emit_expr property)
00029|   | Ast.BinaryExpression { operator; left; right } ->
00030|       Printf.sprintf "%s %s %s" (emit_expr left) operator (emit_expr right)
00031|   | Ast.LogicalExpression { operator; left; right } ->
00032|       Printf.sprintf "%s %s %s" (emit_expr left) operator (emit_expr right)
00033|   | Ast.NewExpression { callee; arguments } ->
00034|       Printf.sprintf "new %s(%s)" (emit_expr callee)
00035|         (String.concat ", " (List.map emit_expr arguments))
00036|   | Ast.ArrowFunctionExpression { params; body; async_ } ->
00037|       let params_str =
00038|         Printf.sprintf "(%s)" (String.concat ", " (List.map emit_expr params))
00039|       in
00040|     ...
readcompleted
lib/emit.mli
Output
<file>
00001| (** AST to TypeScript source code emitter.
00002| 
00003|     This module converts the AST back to readable TypeScript source code,
00004|     which is useful for debugging, pretty-printing, and code generation. *)
00005| 
00006| val emit_expr : Ast.node -> string
00007| (** [emit_expr node] emits an expression node as inline TypeScript
00008|     (no leading indentation). Also handles block statements and
00009|     other compound nodes. *)
00010| 
00011| val emit_stmt : int -> Ast.node -> string
00012| (** [emit_stmt level node] emits a statement node with leading
00013|     indentation at the given level (2 spaces per level). *)
00014| 
00015| val emit_node : Ast.node -> string
00016| (** [emit_node node] converts an AST node to TypeScript source code
00017|     at the top level (indentation level 0).
00018| 
00019|     The output is properly indented (2 spaces) and follows standard
00020|     TypeScript formatting conventions. *)
00021| 

(End of file - total 21 lines)
</file>
4,223in / 1,064out
assistantFeb 10, 02:29:45 AM
claude-opus-4.6 · github-copilot

Now let me read the remaining library files and all test files:

readcompleted
lib/cloudlang.ml
Output
<file>
00001| module Ast = Ast
00002| module Ir = Ir
00003| module Scope = Scope
00004| module Decompose = Decompose
00005| module Parse = Parse
00006| module Subprocess = Subprocess
00007| module Emit = Emit
00008| module Lower = Lower
00009| module Wrangler = Wrangler
00010| 
00011| type error = string
00012| 
00013| (** Parse a TypeScript file into an AST.
00014| 
00015|     Runs the typescript-estree bridge via bun subprocess. *)
00016| let parse_file ?(tools_dir = "tools") (filepath : string) :
00017|     (Ast.node, error) result =
00018|   match Subprocess.run_bridge ~tools_dir filepath with
00019|   | Error e -> Error (Printf.sprintf "parse error: %s" e)
00020|   | Ok json -> (
00021|       match Parse.decode json with
00022|       | Error e -> Error (Printf.sprintf "parse error: %s" e)
00023|       | Ok node -> Ok node)
00024| 
00025| (** Analyse a TypeScript AST and decompose it into Worker groups. *)
00026| let analyze (program : Ast.node) : (Scope.analysis_result, error) result =
00027|   Scope.analyze program
00028|   |> Result.map_error (fun err ->
00029|       Printf.sprintf "analysis error: %s" (Scope.error_to_string err))
00030| 
00031| (** Decompose analysis results into Worker groups and convert to IR.
00032|     Uses stub scripts (handler comments only). *)
00033| let compile (analysis : Scope.analysis_result) : (Ir.config, error) result =
00034|   let groups = Decompose.decompose analysis in
00035|   Ok (Decompose.to_ir groups analysis)
00036| 
00037| (** Decompose and compile with real function body lowering.
00038|     Produces Worker scripts with rewritten bodies, KV calls, service
00039|     fetch calls, and a fetch router. Requires the original program AST. *)
00040| let compile_lowered (program : Ast.node) (analysis : Scope.analysis_result) :
00041|     (Ir.config, error) result =
00042|   let groups = Decompose.decompose analysis in
00043|   let program_body =
00044|     match program with Ast.Program { body } -> body | _ -> []
00045|   in...
readcompleted
lib/cloudlang.mli
Output
<file>
00001| (** CloudLang: TypeScript -> Cloudflare Worker decomposition compiler.
00002| 
00003|     Pipeline: 1. [parse_file]: TypeScript file -> ESTree AST
00004|     2. [analyze]: AST -> scope/closure analysis
00005|     3. [compile]: analysis result -> IR config (Workers + bindings)
00006|     4. [generate]: IR config -> output string (wrangler configs + TS scripts) *)
00007| 
00008| module Ast = Ast
00009| module Ir = Ir
00010| module Scope = Scope
00011| module Decompose = Decompose
00012| module Parse = Parse
00013| module Subprocess = Subprocess
00014| module Emit = Emit
00015| module Lower = Lower
00016| module Wrangler = Wrangler
00017| 
00018| type error = string
00019| 
00020| val parse_file : ?tools_dir:string -> string -> (Ast.node, error) result
00021| (** [parse_file ~tools_dir filepath] parses a TypeScript file into an AST.
00022|     Runs the typescript-estree bridge via bun subprocess.
00023|     @param tools_dir The directory containing ts-to-ast.ts (default: "tools")
00024|     @param filepath The TypeScript file to parse *)
00025| 
00026| val analyze : Ast.node -> (Scope.analysis_result, error) result
00027| 
00028| val compile : Scope.analysis_result -> (Ir.config, error) result
00029| (** Decompose and compile with stub scripts (handler comments only). *)
00030| 
00031| val compile_lowered : Ast.node -> Scope.analysis_result -> (Ir.config, error) result
00032| (** [compile_lowered program analysis] decomposes and compiles with real
00033|     function body lowering. Produces Worker scripts with rewritten bodies,
00034|     KV API calls, service fetch calls, and a fetch router.
00035|     Requires the original program AST for extracting function bodies. *)
00036| 
00037| val generate : Ir.config -> string
00038| 

(End of file - total 38 lines)
</file>
readcompleted
lib/string_list.ml
Output
<file>
00001| (** Deduplicate a string list while preserving first-occurrence order. *)
00002| let dedup_preserve_order (xs : string list) : string list =
00003|   let seen = Hashtbl.create (List.length xs) in
00004|   List.rev
00005|     (List.fold_left
00006|        (fun acc x ->
00007|          if Hashtbl.mem seen x then acc
00008|          else (
00009|            Hashtbl.add seen x ();
00010|            x :: acc))
00011|        [] xs)
00012| 

(End of file - total 12 lines)
</file>
readcompleted
lib/string_list.mli
Output
<file>
00001| (** String list helpers. *)
00002| 
00003| val dedup_preserve_order : string list -> string list
00004| (** [dedup_preserve_order xs] removes duplicate values from [xs], preserving
00005|     first-occurrence order. *)
00006| 

(End of file - total 6 lines)
</file>
readcompleted
lib/subprocess.ml
Output
<file>
00001| (** Subprocess execution for the TypeScript-to-AST bridge.
00002| 
00003|     Shells out to bun to run the typescript-estree bridge script,
00004|     capturing the JSON AST output. *)
00005| 
00006| let run_bridge ?(tools_dir = "tools") (filepath : string) :
00007|     (string, string) result =
00008|   let cmd = Printf.sprintf "bun run %s/ts-to-ast.ts %s" tools_dir filepath in
00009|   let ic = Unix.open_process_in cmd in
00010|   let output = In_channel.input_all ic in
00011|   let status = Unix.close_process_in ic in
00012|   match status with
00013|   | Unix.WEXITED 0 -> Ok output
00014|   | Unix.WEXITED n -> Error (Printf.sprintf "bridge exited with code %d" n)
00015|   | Unix.WSIGNALED n -> Error (Printf.sprintf "bridge killed by signal %d" n)
00016|   | Unix.WSTOPPED n -> Error (Printf.sprintf "bridge stopped by signal %d" n)
00017| 

(End of file - total 17 lines)
</file>
readcompleted
lib/subprocess.mli
Output
<file>
00001| (** Subprocess execution for the TypeScript-to-AST bridge. *)
00002| 
00003| val run_bridge : ?tools_dir:string -> string -> (string, string) result
00004| (** [run_bridge ~tools_dir filepath] executes the bun bridge script on
00005|     [filepath], returning the JSON output or an error message.
00006|     
00007|     @param tools_dir The directory containing ts-to-ast.ts (default: "tools")
00008|     @param filepath The TypeScript file to parse
00009|     @return JSON string on success, error message on failure *)
00010| 

(End of file - total 10 lines)
</file>
readcompleted
lib/wrangler.ml
Output
<file>
00001| (** Wrangler TOML configuration generation.
00002| 
00003|     Generates real wrangler.toml files from IR config that Miniflare
00004|     and wrangler can consume. *)
00005| 
00006| (** Generate a wrangler.toml string for a single Worker resource. *)
00007| let generate_toml (w : Ir.worker) : string =
00008|   let buf = Buffer.create 256 in
00009|   Buffer.add_string buf (Printf.sprintf "name = \"%s\"\n" w.name);
00010|   Buffer.add_string buf "main = \"index.ts\"\n";
00011|   Buffer.add_string buf "compatibility_date = \"2024-01-01\"\n";
00012|   (* KV namespaces *)
00013|   let kv_bindings =
00014|     List.filter_map
00015|       (function
00016|         | Ir.KVBinding { name; namespace_id } -> Some (name, namespace_id)
00017|         | _ -> None)
00018|       w.bindings
00019|   in
00020|   if kv_bindings <> [] then (
00021|     Buffer.add_char buf '\n';
00022|     List.iter
00023|       (fun (name, ns_id) ->
00024|         Buffer.add_string buf "[[kv_namespaces]]\n";
00025|         Buffer.add_string buf (Printf.sprintf "binding = \"%s\"\n" name);
00026|         Buffer.add_string buf (Printf.sprintf "id = \"%s\"\n" ns_id);
00027|         Buffer.add_char buf '\n')
00028|       kv_bindings);
00029|   (* Service bindings *)
00030|   let svc_bindings =
00031|     List.filter_map
00032|       (function
00033|         | Ir.ServiceBinding { name; service } -> Some (name, service)
00034|         | _ -> None)
00035|       w.bindings
00036|   in
00037|   if svc_bindings <> [] then (
00038|     Buffer.add_char buf '\n';
00039|     List.iter
00040|       (fun (name, service) ->
00041|         Buffer.add_string buf "[[services]]\n";
00042|         Buffer.add_string buf (Printf.sprintf "binding = \"%s\"\n" name);
00043|         Buffer.add_string buf (Printf.sprintf "service = \"%s\"\n" service);
00044|         Buffer.add_char buf '\n')
00045|       svc_bindings);
00046|   (* Routes *)
00047|   if w.routes <> [] then (
00048|     Buffer.add_char buf '\n';
00049|...
readcompleted
lib/wrangler.mli
Output
<file>
00001| (** Wrangler TOML configuration generation. *)
00002| 
00003| val generate_toml : Ir.worker -> string
00004| (** [generate_toml worker] generates a wrangler.toml string. *)
00005| 
00006| val generate_project : Ir.config -> (string * string * string) list
00007| (** [generate_project config] returns (dir_name, toml, script) triples
00008|     for each Worker in the config. *)
00009| 

(End of file - total 9 lines)
</file>
readcompleted
bin/main.ml
Output
<file>
00001| let () =
00002|   let args = Array.to_list Sys.argv |> List.tl in
00003|   let lower = List.mem "--lower" args in
00004|   let emit_project = List.mem "--emit-project" args in
00005|   let out_dir =
00006|     let rec find = function
00007|       | "--out-dir" :: dir :: _ -> Some dir
00008|       | _ :: rest -> find rest
00009|       | [] -> None
00010|     in
00011|     find args
00012|   in
00013|   let flags = [ "--lower"; "--emit-project"; "--out-dir" ] in
00014|   let files =
00015|     let rec filter = function
00016|       | [] -> []
00017|       | "--out-dir" :: _ :: rest -> filter rest
00018|       | s :: rest ->
00019|           if List.mem s flags then filter rest else s :: filter rest
00020|     in
00021|     filter args
00022|   in
00023|   match files with
00024|   | [] ->
00025|       print_endline
00026|         "Usage: cloudlang [--lower] [--emit-project --out-dir DIR] \
00027|          <file.ts>\n\n\
00028|          Decomposes a TypeScript program into Cloudflare Workers.\n\n\
00029|          Options:\n\
00030|         \  --lower         Generate real function bodies (not stubs)\n\
00031|         \  --emit-project  Write Worker files to disk\n\
00032|         \  --out-dir DIR   Output directory (default: ./out)"
00033|   | filename :: _ -> (
00034|       match Cloudlang.parse_file filename with
00035|       | Error e -> Printf.eprintf "Error: %s\n" e
00036|       | Ok ast -> (
00037|           match Cloudlang.analyze ast with
00038|           | Error e -> Printf.eprintf "Error: %s\n" e
00039|           | Ok analysis -> (
00040|               let compile_result =
00041|                 if lower || emit_project then
00042|                   Cloudlang.compile_lowered ast analysis
00043|                 else Cloudlang.compile analysis
00044|               in
00045|               match compile_result with
00046|               | Error e -> Printf.eprintf "Error: %s\n" e
00047|               | Ok config ->
00048|                   if emit_pr...
29,980in / 560out
assistantFeb 10, 02:29:52 AM
claude-opus-4.6 · github-copilot

Now let me read all the test files:

readcompleted
test/test_cloudlang.ml
Output
<file>
00001| let () =
00002|   print_endline "Running CloudLang tests...";
00003| 
00004|   Test_scope.run ();
00005|   print_endline "  scope analysis: OK";
00006| 
00007|   Test_decompose.run ();
00008|   print_endline "  decomposition: OK";
00009| 
00010|   Test_ir.run ();
00011|   print_endline "  IR output: OK";
00012| 
00013|   Test_durable_object_hints.run ();
00014|   print_endline "  durable object hints: OK";
00015| 
00016|   Test_generate.run ();
00017|   print_endline "  generated output: OK";
00018| 
00019|   Test_parse.run ();
00020|   print_endline "  parse integration: OK";
00021| 
00022|   Test_task_api.run ();
00023|   print_endline "  task API decomposition: OK";
00024| 
00025|   Test_emit.run ();
00026|   print_endline "  AST emitter: OK";
00027| 
00028|   Test_lower.run ();
00029|   print_endline "  body lowering: OK";
00030| 
00031|   print_endline "";
00032|   print_endline "All tests passed!"
00033| 

(End of file - total 33 lines)
</file>
readcompleted
test/fixture.ml
Output
<file>
00001| module A = Cloudlang.Ast
00002| 
00003| let has_substr s sub =
00004|   let len_s = String.length s in
00005|   let len_sub = String.length sub in
00006|   if len_sub > len_s then false
00007|   else
00008|     let found = ref false in
00009|     for i = 0 to len_s - len_sub do
00010|       if String.sub s i len_sub = sub then found := true
00011|     done;
00012|     !found
00013| 
00014| let id name = A.Identifier { name }
00015| let str s = A.Literal { value = A.LString s }
00016| let undef = A.Literal { value = A.LUndefined }
00017| 
00018| let const_decl name init =
00019|   A.VariableDeclaration
00020|     {
00021|       kind = A.Const;
00022|       declarations = [ A.VariableDeclarator { id = id name; init } ];
00023|     }
00024| 
00025| let arrow params body =
00026|   A.ArrowFunctionExpression { params; body; async_ = false }
00027| 
00028| let call callee arguments = A.CallExpression { callee; arguments }
00029| let member obj prop = A.MemberExpression { object_ = obj; property = id prop }
00030| let expr_stmt e = A.ExpressionStatement { expression = e }
00031| let block stmts = A.BlockStatement { body = stmts }
00032| let ret e = A.ReturnStatement { argument = Some e }
00033| 
00034| let if_ test consequent alternate =
00035|   A.IfStatement { test; consequent; alternate }
00036| 
00037| let bin op left right = A.BinaryExpression { operator = op; left; right }
00038| let logic op left right = A.LogicalExpression { operator = op; left; right }
00039| let new_ callee arguments = A.NewExpression { callee; arguments }
00040| 
00041| let export_named names =
00042|   A.ExportNamedDeclaration
00043|     {
00044|       specifiers = List.map (fun n -> A.ExportSpecifier { local = id n }) names;
00045|     }
00046| 
00047| let build_kv_store_ast () : A.node =
00048|   A.Program
00049|     {
00050|       body =
00051|         [
00052|           const_decl "store" (Some (new_ (id "Map") []));
00053|           const_decl "put"
00054|             (Some
0...
readcompleted
test/test_parse.ml
Output
<file>
00001| (** Integration test for TypeScript parsing pipeline.
00002| 
00003|     Validates that real TS files parse to the same analysis results as
00004|     the hand-constructed AST in Fixture. *)
00005| 
00006| let run () =
00007|   (* Find project root - look for dune-project in parent directories *)
00008|   let rec find_project_root dir =
00009|     if Sys.file_exists (Filename.concat dir "dune-project") then dir
00010|     else
00011|       let parent = Filename.dirname dir in
00012|       if parent = dir then failwith "Could not find project root"
00013|       else find_project_root parent
00014|   in
00015|   let project_root = find_project_root (Sys.getcwd ()) in
00016|   let tools_dir = Filename.concat project_root "tools" in
00017|   let filepath = Filename.concat project_root "examples/kv-store.ts" in
00018| 
00019|   let ast_from_file =
00020|     match Cloudlang.parse_file ~tools_dir filepath with
00021|     | Ok ast -> ast
00022|     | Error e -> failwith (Printf.sprintf "parse_file failed: %s" e)
00023|   in
00024| 
00025|   let ast_from_fixture = Fixture.build_kv_store_ast () in
00026| 
00027|   (* Both should analyze successfully *)
00028|   let analysis_from_file =
00029|     match Cloudlang.analyze ast_from_file with
00030|     | Ok a -> a
00031|     | Error e -> failwith (Printf.sprintf "analyze (file) failed: %s" e)
00032|   in
00033| 
00034|   let analysis_from_fixture =
00035|     match Cloudlang.analyze ast_from_fixture with
00036|     | Ok a -> a
00037|     | Error e -> failwith (Printf.sprintf "analyze (fixture) failed: %s" e)
00038|   in
00039| 
00040|   (* Same number of closures detected *)
00041|   assert (List.length analysis_from_file.closures = List.length analysis_from_fixture.closures);
00042| 
00043|   (* Same exports *)
00044|   assert (analysis_from_file.exports = analysis_from_fixture.exports);
00045|   assert (analysis_from_file.exports = [ "handleRequest" ]);
00046| 
00047|   (* Same closure count: put, get, cachedGet...
readcompleted
test/test_scope.ml
Output
<file>
00001| let run () =
00002|   let program = Fixture.build_kv_store_ast () in
00003|   let analysis = Fixture.analyze_exn program in
00004| 
00005|   assert (List.length analysis.module_vars = 6);
00006| 
00007|   let get_var name =
00008|     List.find
00009|       (fun (v : Cloudlang.Scope.var_info) -> v.name = name)
00010|       analysis.module_vars
00011|   in
00012|   assert (get_var "store").is_mutable_state;
00013|   assert (get_var "cache").is_mutable_state;
00014|   assert (not (get_var "put").is_mutable_state);
00015|   assert (get_var "put").is_function;
00016|   assert (get_var "get").is_function;
00017|   assert (get_var "cachedGet").is_function;
00018|   assert (get_var "handleRequest").is_function;
00019| 
00020|   assert (List.length analysis.closures = 4);
00021| 
00022|   let get_closure name =
00023|     List.find
00024|       (fun (c : Cloudlang.Scope.closure_info) -> c.name = name)
00025|       analysis.closures
00026|   in
00027| 
00028|   let c_put = get_closure "put" in
00029|   assert (c_put.free_vars = [ "store" ]);
00030|   assert (c_put.captures_mutable = [ "store" ]);
00031|   assert (c_put.called_functions = []);
00032| 
00033|   let c_get = get_closure "get" in
00034|   assert (c_get.free_vars = [ "store" ]);
00035|   assert (c_get.captures_mutable = [ "store" ]);
00036|   assert (c_get.called_functions = []);
00037| 
00038|   let c_cached = get_closure "cachedGet" in
00039|   assert (List.mem "cache" c_cached.free_vars);
00040|   assert (List.mem "get" c_cached.free_vars);
00041|   assert (c_cached.captures_mutable = [ "cache" ]);
00042|   assert (List.mem "get" c_cached.called_functions);
00043| 
00044|   let c_handle = get_closure "handleRequest" in
00045|   assert (List.mem "cachedGet" c_handle.free_vars);
00046|   assert (List.mem "put" c_handle.free_vars);
00047|   assert (c_handle.captures_mutable = []);
00048|   assert (List.mem "cachedGet" c_handle.called_functions);
00049|   assert (List.mem "put" c_handle.called_functio...
readcompleted
test/test_ir.ml
Output
<file>
00001| let get_worker name config =
00002|   match
00003|     List.find_map
00004|       (function
00005|         | Cloudlang.Ir.Worker w when w.name = name -> Some w | _ -> None)
00006|       config
00007|   with
00008|   | Some w -> w
00009|   | None -> failwith (Printf.sprintf "missing worker: %s" name)
00010| 
00011| let run () =
00012|   let program = Fixture.build_kv_store_ast () in
00013|   let analysis = Fixture.analyze_exn program in
00014|   let config = Fixture.compile_exn analysis in
00015| 
00016|   assert (List.length config = 3);
00017| 
00018|   List.iter
00019|     (function
00020|       | Cloudlang.Ir.Worker _ -> ()
00021|       | _ -> failwith "expected only Worker resources")
00022|     config;
00023| 
00024|   let w_storage = get_worker "storage" config in
00025|   assert (
00026|     List.exists
00027|       (function
00028|         | Cloudlang.Ir.KVBinding { name = "STORE"; namespace_id } ->
00029|             namespace_id = "cloudlang-store"
00030|         | _ -> false)
00031|       w_storage.bindings);
00032|   assert (w_storage.routes = []);
00033| 
00034|   let w_cache = get_worker "cache" config in
00035|   assert (
00036|     List.exists
00037|       (function
00038|         | Cloudlang.Ir.KVBinding { name = "CACHE"; namespace_id } ->
00039|             namespace_id = "cloudlang-cache"
00040|         | _ -> false)
00041|       w_cache.bindings);
00042|   assert (
00043|     List.exists
00044|       (function
00045|         | Cloudlang.Ir.ServiceBinding { name = "STORAGE"; service } ->
00046|             service = "storage"
00047|         | _ -> false)
00048|       w_cache.bindings);
00049|   assert (w_cache.routes = []);
00050| 
00051|   let w_api = get_worker "handleRequest" config in
00052|   assert (
00053|     List.exists
00054|       (function
00055|         | Cloudlang.Ir.ServiceBinding { name = "CACHE"; service } ->
00056|             service = "cache"
00057|         | _ -> false)
00058|       w_api.bindings);
00059|   asse...
readcompleted
test/test_decompose.ml
Output
<file>
00001| let run () =
00002|   let program = Fixture.build_kv_store_ast () in
00003|   let analysis = Fixture.analyze_exn program in
00004|   let groups = Cloudlang.Decompose.decompose analysis in
00005| 
00006|   assert (List.length groups = 3);
00007| 
00008|   let get_group name =
00009|     List.find
00010|       (fun (g : Cloudlang.Decompose.worker_group) -> g.name = name)
00011|       groups
00012|   in
00013| 
00014|   let g_storage = get_group "storage" in
00015|   assert (List.mem "put" g_storage.functions);
00016|   assert (List.mem "get" g_storage.functions);
00017|   assert (g_storage.owned_state = [ "store" ]);
00018|   assert (g_storage.service_deps = []);
00019| 
00020|   let g_cache = get_group "cache" in
00021|   assert (g_cache.functions = [ "cachedGet" ]);
00022|   assert (g_cache.owned_state = [ "cache" ]);
00023|   assert (List.mem "storage" g_cache.service_deps);
00024| 
00025|   let g_api = get_group "handleRequest" in
00026|   assert (g_api.functions = [ "handleRequest" ]);
00027|   assert (g_api.owned_state = []);
00028|   assert (List.mem "cache" g_api.service_deps);
00029|   assert (List.mem "storage" g_api.service_deps)
00030| 

(End of file - total 30 lines)
</file>
readcompleted
test/test_emit.ml
Output
<file>
00001| (** Tests for the AST-to-TypeScript emitter. *)
00002| 
00003| let run () =
00004|   (* Simple identifier *)
00005|   let id = Cloudlang.Ast.Identifier { name = "x" } in
00006|   assert (Cloudlang.Emit.emit_expr id = "x");
00007| 
00008|   (* String literal *)
00009|   let s = Cloudlang.Ast.Literal { value = Cloudlang.Ast.LString "hello" } in
00010|   assert (Cloudlang.Emit.emit_expr s = {|"hello"|});
00011| 
00012|   (* Number literal (integer) *)
00013|   let n = Cloudlang.Ast.Literal { value = Cloudlang.Ast.LNumber 42.0 } in
00014|   assert (Cloudlang.Emit.emit_expr n = "42");
00015| 
00016|   (* Boolean literal *)
00017|   let b = Cloudlang.Ast.Literal { value = Cloudlang.Ast.LBool true } in
00018|   assert (Cloudlang.Emit.emit_expr b = "true");
00019| 
00020|   (* Binary expression *)
00021|   let bin =
00022|     Cloudlang.Ast.BinaryExpression
00023|       {
00024|         operator = "+";
00025|         left = Cloudlang.Ast.Identifier { name = "a" };
00026|         right = Cloudlang.Ast.Literal { value = Cloudlang.Ast.LNumber 1.0 };
00027|       }
00028|   in
00029|   assert (Cloudlang.Emit.emit_expr bin = "a + 1");
00030| 
00031|   (* Call expression *)
00032|   let call =
00033|     Cloudlang.Ast.CallExpression
00034|       {
00035|         callee = Cloudlang.Ast.Identifier { name = "foo" };
00036|         arguments =
00037|           [
00038|             Cloudlang.Ast.Identifier { name = "x" };
00039|             Cloudlang.Ast.Literal { value = Cloudlang.Ast.LString "y" };
00040|           ];
00041|       }
00042|   in
00043|   assert (Cloudlang.Emit.emit_expr call = {|foo(x, "y")|});
00044| 
00045|   (* Member expression *)
00046|   let mem =
00047|     Cloudlang.Ast.MemberExpression
00048|       {
00049|         object_ = Cloudlang.Ast.Identifier { name = "obj" };
00050|         property = Cloudlang.Ast.Identifier { name = "prop" };
00051|       }
00052|   in
00053|   assert (Cloudlang.Emit.emit_expr mem = "obj.prop");
00054| 
00055|   (* Aw...
readcompleted
test/test_lower.ml
Output
<file>
00001| (** Tests for the body lowering pass. *)
00002| 
00003| let run () =
00004|   (* Test rewrite_node: Map.get -> KV get with await *)
00005|   let ctx : Cloudlang.Lower.rewrite_ctx =
00006|     { owned_state = [ "tasks" ]; service_deps = [] }
00007|   in
00008| 
00009|   (* tasks.get(key) -> await env.TASKS.get(key) *)
00010|   let map_get =
00011|     Cloudlang.Ast.CallExpression
00012|       {
00013|         callee =
00014|           Cloudlang.Ast.MemberExpression
00015|             {
00016|               object_ = Cloudlang.Ast.Identifier { name = "tasks" };
00017|               property = Cloudlang.Ast.Identifier { name = "get" };
00018|             };
00019|         arguments = [ Cloudlang.Ast.Identifier { name = "key" } ];
00020|       }
00021|   in
00022|   let rewritten = Cloudlang.Lower.rewrite_node ctx map_get in
00023|   let emitted = Cloudlang.Emit.emit_expr rewritten in
00024|   assert (Fixture.has_substr emitted "await");
00025|   assert (Fixture.has_substr emitted "env.TASKS.get");
00026|   assert (Fixture.has_substr emitted "key");
00027| 
00028|   (* tasks.set(key, value) -> await env.TASKS.put(key, value) *)
00029|   let map_set =
00030|     Cloudlang.Ast.CallExpression
00031|       {
00032|         callee =
00033|           Cloudlang.Ast.MemberExpression
00034|             {
00035|               object_ = Cloudlang.Ast.Identifier { name = "tasks" };
00036|               property = Cloudlang.Ast.Identifier { name = "set" };
00037|             };
00038|         arguments =
00039|           [
00040|             Cloudlang.Ast.Identifier { name = "key" };
00041|             Cloudlang.Ast.Identifier { name = "value" };
00042|           ];
00043|       }
00044|   in
00045|   let rewritten = Cloudlang.Lower.rewrite_node ctx map_set in
00046|   let emitted = Cloudlang.Emit.emit_expr rewritten in
00047|   assert (Fixture.has_substr emitted "await");
00048|   assert (Fixture.has_substr emitted "env.TASKS.put");
00049| 
00050|   (* tasks.delete(ke...
readcompleted
test/test_generate.ml
Output
<file>
00001| let run () =
00002|   let program = Fixture.build_kv_store_ast () in
00003|   let analysis = Fixture.analyze_exn program in
00004|   let config = Fixture.compile_exn analysis in
00005|   let output = Cloudlang.generate config in
00006| 
00007|   assert (Fixture.has_substr output {|"name": "storage"|});
00008|   assert (Fixture.has_substr output {|"name": "cache"|});
00009|   assert (Fixture.has_substr output {|"name": "handleRequest"|});
00010| 
00011|   assert (Fixture.has_substr output {|"binding": "STORE"|});
00012|   assert (Fixture.has_substr output {|"id": "cloudlang-store"|});
00013|   assert (Fixture.has_substr output {|"binding": "CACHE"|});
00014|   assert (Fixture.has_substr output {|"id": "cloudlang-cache"|});
00015| 
00016|   assert (Fixture.has_substr output {|"service": "storage"|});
00017|   assert (Fixture.has_substr output {|"service": "cache"|});
00018| 
00019|   assert (Fixture.has_substr output {|"/handleRequest"|});
00020| 
00021|   assert (Fixture.has_substr output {|"durable_object_hint"|});
00022|   assert (Fixture.has_substr output {|"fit_score"|});
00023| 
00024|   assert (Fixture.has_substr output "interface Env");
00025|   assert (Fixture.has_substr output "async fetch");
00026|   assert (Fixture.has_substr output "export default")
00027| 

(End of file - total 27 lines)
</file>
readcompleted
test/test_durable_object_hints.ml
Output
<file>
00001| let get_worker name config =
00002|   match
00003|     List.find_map
00004|       (function
00005|         | Cloudlang.Ir.Worker w when w.name = name -> Some w | _ -> None)
00006|       config
00007|   with
00008|   | Some w -> w
00009|   | None -> failwith (Printf.sprintf "missing worker: %s" name)
00010| 
00011| let run () =
00012|   let program = Fixture.build_kv_store_ast () in
00013|   let analysis = Fixture.analyze_exn program in
00014|   let config = Fixture.compile_exn analysis in
00015| 
00016|   let w_storage = get_worker "storage" config in
00017|   let w_cache = get_worker "cache" config in
00018|   let w_api = get_worker "handleRequest" config in
00019| 
00020|   (match w_storage.do_annotation with
00021|   | None -> failwith "expected DO hint for storage worker"
00022|   | Some hint ->
00023|       assert (hint.do_fit_score >= 60);
00024|       assert (List.mem Cloudlang.Ir.SerialConflictHotspot hint.markers));
00025| 
00026|   (match w_cache.do_annotation with
00027|   | None -> failwith "expected DO hint for cache worker"
00028|   | Some hint ->
00029|       assert (hint.do_fit_score >= 60);
00030|       assert (List.mem Cloudlang.Ir.StrictOrderingRequired hint.markers));
00031| 
00032|   assert (w_api.do_annotation = None)
00033| 

(End of file - total 33 lines)
</file>
readcompleted
test/test_task_api.ml
Output
<file>
00001| (** Tests for the task-api.ts decomposition.
00002| 
00003|     Verifies that CloudLang correctly decomposes the task API into:
00004|     - A "tasks_nextId" Worker group with all CRUD functions + KV bindings
00005|     - A "handleRequest" Worker group with service binding to tasks_nextId *)
00006| 
00007| let run () =
00008|   let program = Fixture.build_task_api_ast () in
00009|   let analysis = Fixture.analyze_exn program in
00010| 
00011|   (* --- Scope analysis --- *)
00012| 
00013|   (* 7 module-scope vars: tasks, nextId, createTask, getTask,
00014|      updateTask, deleteTask, handleRequest *)
00015|   assert (List.length analysis.module_vars = 7);
00016| 
00017|   let get_var name =
00018|     List.find
00019|       (fun (v : Cloudlang.Scope.var_info) -> v.name = name)
00020|       analysis.module_vars
00021|   in
00022|   assert (get_var "tasks").is_mutable_state;
00023|   assert (get_var "nextId").is_mutable_state;
00024|   assert (get_var "createTask").is_function;
00025|   assert (get_var "getTask").is_function;
00026|   assert (get_var "updateTask").is_function;
00027|   assert (get_var "deleteTask").is_function;
00028|   assert (get_var "handleRequest").is_function;
00029| 
00030|   (* 5 closures (one per arrow function) *)
00031|   assert (List.length analysis.closures = 5);
00032| 
00033|   let get_closure name =
00034|     List.find
00035|       (fun (c : Cloudlang.Scope.closure_info) -> c.name = name)
00036|       analysis.closures
00037|   in
00038| 
00039|   (* createTask captures both tasks and nextId *)
00040|   let c_create = get_closure "createTask" in
00041|   assert (List.mem "tasks" c_create.captures_mutable);
00042|   assert (List.mem "nextId" c_create.captures_mutable);
00043|   assert (c_create.called_functions = []);
00044| 
00045|   (* getTask captures tasks only *)
00046|   let c_get = get_closure "getTask" in
00047|   assert (c_get.captures_mutable = [ "tasks" ]);
00048|   assert (c_get.called_functions = []);
00049| 
...
7,883in / 684out
assistantFeb 10, 02:30:00 AM
claude-opus-4.6 · github-copilot

Now let me read the remaining files - examples, tools, and context docs:

readcompleted
examples/kv-store.ts
Output
<file>
00001| // examples/kv-store.ts
00002| //
00003| // A key-value store with a cache layer.
00004| // Written as a normal TypeScript program — no Cloudflare awareness.
00005| //
00006| // CloudLang decomposes this into 3 Workers:
00007| //   1. "storage"        — put/get functions, KV binding for store
00008| //   2. "cache"          — cachedGet function, KV + service binding
00009| //   3. "handleRequest"  — routing layer, service bindings to cache + storage
00010| 
00011| const store = new Map<string, string>();
00012| 
00013| const put = (key: string, value: string): void => {
00014|   store.set(key, value);
00015| };
00016| 
00017| const get = (key: string): string | undefined => {
00018|   return store.get(key);
00019| };
00020| 
00021| const cache = new Map<string, string>();
00022| 
00023| const cachedGet = (key: string): string | undefined => {
00024|   const hit = cache.get(key);
00025|   if (hit) return hit;
00026|   const result = get(key);
00027|   if (result) {
00028|     cache.set(key, result);
00029|     return result;
00030|   }
00031|   return undefined;
00032| };
00033| 
00034| const handleRequest = (method: string, key: string, value?: string): string => {
00035|   if (method === "GET") {
00036|     return cachedGet(key) ?? "NOT_FOUND";
00037|   }
00038|   if (method === "PUT" && value) {
00039|     put(key, value);
00040|     return "OK";
00041|   }
00042|   return "BAD_REQUEST";
00043| };
00044| 
00045| export { handleRequest };
00046| 

(End of file - total 46 lines)
</file>
readcompleted
examples/hello.cl
Output
<file>
00001| # Placeholder CloudLang example
00002| define hello = "Hello, Cloudflare!"
00003| export hello
00004| 

(End of file - total 4 lines)
</file>
readcompleted
examples/closure_worker.cl
Output
<file>
00001| # Example: Closure capturing a D1 resource -> Worker with D1 binding
00002| #
00003| # This demonstrates the core CloudLang transformation:
00004| #   1. A D1 database resource is defined via the `d1` primitive
00005| #   2. A closure (lambda) captures that resource as a free variable
00006| #   3. The compiler detects the capture and generates:
00007| #      - A D1Database IR resource
00008| #      - A Worker IR resource with a D1 binding in its env
00009| #      - TypeScript Worker script that accesses env.USERS_DB
00010| #   4. Exporting the closure attaches a route to the Worker
00011| 
00012| define users_db = d1 "users-db" "CREATE TABLE users (id INT, name TEXT)"
00013| 
00014| define get_user = \id : string ->
00015|   query users_db "SELECT * FROM users WHERE id = ?" id
00016| 
00017| export get_user
00018| 

(End of file - total 18 lines)
</file>
readcompleted
tools/ts-to-ast.ts
Output
<file>
00001| import { parse } from "@typescript-eslint/typescript-estree";
00002| import { readFileSync } from "fs";
00003| 
00004| const filepath = process.argv[2];
00005| if (!filepath) {
00006| 	console.error("Usage: ts-to-ast.ts <filepath>");
00007| 	process.exit(1);
00008| }
00009| 
00010| const code = readFileSync(filepath, "utf-8");
00011| const ast = parse(code, {
00012| 	loc: false,
00013| 	range: false,
00014| 	tokens: false,
00015| 	comment: false,
00016| });
00017| 
00018| console.log(JSON.stringify(ast));
00019| console.clear();
00020| 

(End of file - total 20 lines)
</file>
readcompleted
.ocamlformat
Output
<file>
00001| version = 0.28.1
00002| 

(End of file - total 2 lines)
</file>
readcompleted
.github/workflows/ci.yml
Output
<file>
00001| name: ci
00002| 
00003| on:
00004|   push:
00005|   pull_request:
00006| 
00007| jobs:
00008|   build-and-test:
00009|     runs-on: ubuntu-latest
00010|     steps:
00011|       - name: Checkout
00012|         uses: actions/checkout@v4
00013| 
00014|       - name: Setup OCaml
00015|         uses: ocaml/setup-ocaml@v3
00016|         with:
00017|           ocaml-compiler: 5.2.0
00018| 
00019|       - name: Install dependencies
00020|         run: opam install . --deps-only --with-test --yes
00021| 
00022|       - name: Check formatting
00023|         run: opam exec -- dune fmt --check
00024| 
00025|       - name: Build
00026|         run: opam exec -- dune build
00027| 
00028|       - name: Run tests
00029|         run: opam exec -- dune runtest
00030| 

(End of file - total 30 lines)
</file>
53,200in / 386out
assistantFeb 10, 02:30:06 AM
claude-opus-4.6 · github-copilot

Now I have a complete picture. Here is the comprehensive analysis:


Comprehensive Project Analysis: CloudLang (react tree)

1. Directory Tree Structure

/home/jm/data/ai/cloudlang-trees/react/
├── .claude/context/
│   └── capnweb-integration.md
├── .github/workflows/
│   └── ci.yml                          # CI: OCaml 5.2.0, fmt + build + test
├── .ocamlformat                        # v0.28.1
├── .opencode/
│   ├── .gitignore
│   ├── package.json
│   └── context/
│       ├── cloudlang-architecture-design.md
│       ├── typescript-ast-json-research.md
│       └── typescript-ast-summary.md
├── AGENTS.md
├── LICENSE.md
├── README.md
├── dune-project
├── cloudlang.opam                      # Generated from dune-project
├── bin/
│   ├── dune
│   └── main.ml                         # CLI entry point
├── lib/
│   ├── dune                            # library "cloudlang" (jsont, jsont.bytesrw, unix)
│   ├── ast.ml / ast.mli                # ESTree AST types
│   ├── ir.ml / ir.mli                  # Cloudflare IR types
│   ├── parse.ml / parse.mli            # JSON -> AST deserializer (jsont)
│   ├── scope.ml / scope.mli            # Scope/closure analysis pass
│   ├── decompose.ml / decompose.mli    # Closure decomposition -> Worker groups -> IR
│   ├── lower.ml / lower.mli            # Body lowering (AST rewriting for distributed exec)
│   ├── emit.ml / emit.mli              # AST -> TypeScript source emitter
│   ├── cloudlang.ml / cloudlang.mli    # Top-level pipeline facade + code generation
│   ├── string_list.ml / string_list.mli# Utility: dedup_preserve_order
│   ├── subprocess.ml / subprocess.mli  # Bun subprocess bridge
│   └── wrangler.ml / wrangler.mli      # Wrangler TOML + project file generation
├── test/
│   ├── dune
│   ├── fixture.ml                      # Shared test fixtures (AST builders)
│   ├── test_cloudlang.ml               # Test runner (9 test suites)
│   ├── test_scope.ml
│   ├── test_decompose.ml
│   ├── test_ir.ml
│   ├── test_durable_object_hints.ml
│   ├── test_generate.ml
│   ├── test_parse.ml                   # Integration test (requires bun)
│   ├── test_emit.ml
│   ├── test_lower.ml
│   └── test_task_api.ml
├── tools/
│   ├── package.json
│   └── ts-to-ast.ts                    # TypeScript bridge: file -> ESTree JSON
├── verify/
│   ├── package.json
│   ├── scenarios.ts
│   └── verify.ts
├── examples/
│   ├── hello.cl                        # Original CloudLang syntax (placeholder)
│   ├── closure_worker.cl               # Original CloudLang syntax (D1 example)
│   ├── kv-store.ts                     # Primary example: KV store with cache
│   ├── task-api.ts
│   └── task-api-server.ts
└── docs/
    └── durable_objects_ir_design.md

2. Dependencies (dune-project & opam)

/home/jm/data/ai/cloudlang-trees/react/dune-project (lines 1-17):

  • Dune language: 3.20
  • Package name: cloudlang
  • Synopsis: "TypeScript to Cloudflare Worker decomposition compiler"
  • Dependencies:
    • ocaml
    • jsont -- Daniel Bunzli's type-safe JSON codec library
    • bytesrw -- byte stream reader/writer (used by jsont.bytesrw)

Library dune file (/home/jm/data/ai/cloudlang-trees/react/lib/dune, line 3):

  • Libraries linked: jsont, jsont.bytesrw, unix

No test framework -- tests use raw assert statements, not Alcotest.

3. Compiler Pipeline

The pipeline is documented in /home/jm/data/ai/cloudlang-trees/react/lib/cloudlang.mli, lines 1-6:

Pipeline:
  1. parse_file   : TypeScript file -> ESTree AST    (via bun subprocess)
  2. analyze      : AST -> scope/closure analysis
  3. compile      : analysis result -> IR config      (Workers + bindings)
  4. generate     : IR config -> output string        (wrangler configs + TS scripts)

There are actually two compile paths (lines 28-35 of cloudlang.mli):

  • compile -- Stub scripts (handler comments only, no real function bodies)
  • compile_lowered -- Full body lowering with rewritten function bodies, KV API calls, service fetch RPCs, and a fetch router

The phases in detail:

Phase Module Input Output File
Parse Parse JSON string (ESTree) Ast.node lib/parse.ml (191 lines)
Scope Analysis Scope Ast.node (Program) Scope.analysis_result lib/scope.ml (240 lines)
Decomposition Decompose Scope.analysis_result Decompose.worker_group list lib/decompose.ml (411 lines)
IR Conversion Decompose.to_ir worker_group list Ir.config lib/decompose.ml, line 376
Body Lowering Lower worker_group + AST bodies Rewritten Ast.node trees lib/lower.ml (329 lines)
Emit Emit Ast.node TypeScript source string lib/emit.ml (122 lines)
Code Gen Cloudlang.generate Ir.config JSON/wrangler output string lib/cloudlang.ml, line 240
Wrangler Wrangler Ir.worker TOML config + project files lib/wrangler.ml (70 lines)

The external bridge is tools/ts-to-ast.ts: it uses @typescript-eslint/typescript-estree to parse TypeScript into ESTree JSON, which is piped back via stdout. The OCaml side calls this via Subprocess.run_bridge (shells out to bun).

4. AST Types

/home/jm/data/ai/cloudlang-trees/react/lib/ast.ml (lines 1-44):

type var_kind = Const | Let | Var                              (* line 7 *)

type literal_value =                                            (* line 9 *)
  | LString of string
  | LNumber of float
  | LBool of bool
  | LNull
  | LUndefined

type node =                                                     (* line 17 *)
  | Program of { body : node list }
  | VariableDeclaration of { kind : var_kind; declarations : node list }
  | VariableDeclarator of { id : node; init : node option }
  | Identifier of { name : string }
  | Literal of { value : literal_value }
  | ArrowFunctionExpression of { params : node list; body : node; async_ : bool }
  | BlockStatement of { body : node list }
  | ReturnStatement of { argument : node option }
  | IfStatement of { test : node; consequent : node; alternate : node option }
  | ExpressionStatement of { expression : node }
  | CallExpression of { callee : node; arguments : node list }
  | MemberExpression of { object_ : node; property : node }
  | NewExpression of { callee : node; arguments : node list }
  | BinaryExpression of { operator : string; left : node; right : node }
  | LogicalExpression of { operator : string; left : node; right : node }
  | AwaitExpression of { argument : node }
  | RawCode of { code : string }                               (* synthetic, line 38 *)
  | ExportNamedDeclaration of { specifiers : node list }
  | ExportSpecifier of { local : node }

This is a minimal ESTree subset -- 19 node variants. Notable: no FunctionDeclaration, ClassDeclaration, ForStatement, WhileStatement, ObjectExpression, ArrayExpression, TemplateLiteral, AssignmentExpression, TSTypeAnnotation, etc. The RawCode variant is synthetic (not from the parser), used by the lowering pass for constructs without AST representation.

5. IR Types

/home/jm/data/ai/cloudlang-trees/react/lib/ir.ml (lines 1-68):

type binding =                                                  (* line 1 *)
  | D1Binding of { name : string; database_id : string }
  | KVBinding of { name : string; namespace_id : string }
  | R2Binding of { name : string; bucket_name : string }
  | ServiceBinding of { name : string; service : string }

type do_marker =                                                (* line 7 *)
  | SerialConflictHotspot
  | GlobalSingletonNeeded
  | EntityAffinityKeyed of { key_hint : string option }
  | StrictOrderingRequired
  | ConsistencyBoundaryCrossed
  | RealtimeFanout
  | DurableTimerNeeded
  | HighFanInCoordinator
  | StatelessFastPathPreferWorker

type coordination_intent =                                      (* line 18 *)
  | PerEntitySerial of { key_expr : string; reason : string }
  | GlobalSingleton of { name : string; reason : string }
  | RealtimeRoom of { room_key_expr : string }
  | ScheduledQueue of { queue_key_expr : string }

type routing_policy =                                           (* line 24 *)
  | DeterministicName of { key_expr : string }
  | ShardedName of { key_expr : string; shards : int }
  | UniqueIdPool of { pool : string }

type storage_backend = DoSqlite | DoSyncKv                     (* line 29 *)
type alarm_mode = NoAlarm | SingleAlarm | QueuedAlarm          (* line 30 *)

type durability_policy = { ... }                                (* line 32 *)
type do_annotation = { ... }                                    (* line 38 *)

type worker = {                                                 (* line 49 *)
  name : string;
  script : string;
  bindings : binding list;
  routes : string list;
  do_annotation : do_annotation option;
}

type durable_object = { class_name : string; script : string } (* line 57 *)
type r2_bucket = { name : string; location : string }          (* line 58 *)
type d1_database = { name : string; schema : string }          (* line 59 *)

type resource =                                                 (* line 61 *)
  | Worker of worker
  | DurableObject of durable_object
  | R2Bucket of r2_bucket
  | D1Database of d1_database

type config = resource list                                     (* line 67 *)

The IR is quite rich. It has a full Durable Object annotation system with markers, coordination intents, routing policies, and durability policies. The do_annotation is an optional field on worker and is inferred heuristically by the decompose pass.

6. Analysis Passes

Scope Analysis (lib/scope.ml, 240 lines)

Three-pass design (documented at line 1):

  1. Declaration pass (collect_module_vars, line 177): Walks top-level VariableDeclaration nodes. For each, records var_info with is_mutable_state (init is new Map/Set/Array) and is_function (init is ArrowFunctionExpression).

  2. Reference pass (collect_closures, line 194): For each top-level arrow function, calls analyze_closure (line 135) which:

    • Extracts parameter names (line 137)
    • Collects local variable declarations (line 138, collect_locals)
    • Collects all identifier references in the body (collect_refs, line 53) -- recursive traversal, skips nested arrows, skips member expression properties
    • Filters references to find free variables: not in params/locals AND exists in module_vars
    • Collects called functions (collect_called_functions, line 90): only direct Identifier callees (not obj.method())
    • Identifies captured mutable state: subset of free_vars that are is_mutable_state
  3. Export pass (collect_exports, line 217): Extracts names from ExportNamedDeclaration / ExportSpecifier nodes.

Entry point: Scope.analyze (line 232), returns analysis_result = { closures; module_vars; exports }.

Decomposition (lib/decompose.ml, 411 lines)

Three-step algorithm (documented at line 1):

  1. Group seeding (lines 227-282): Functions sharing mutable state variables are merged into the same worker_group. Uses iterative merging when a function captures multiple state vars (causes those vars' groups to merge).

  2. Standalone groups (lines 283-301): Functions with no mutable captures become their own 1-function group.

  3. Cross-group service dependencies (lines 302-325): For each group, checks its functions' called_functions and finds which of those are in different groups. Those become service_deps.

DO Annotation Inference (lib/decompose.ml)

  • infer_markers (line 58): Heuristic marker inference based on group structure:

    • Has owned state -> SerialConflictHotspot, StrictOrderingRequired, EntityAffinityKeyed
    • Multiple functions + state -> GlobalSingletonNeeded
    • State + cross-service deps -> ConsistencyBoundaryCrossed
    • 2+ service deps -> HighFanInCoordinator
    • Name-based heuristics for RealtimeFanout, DurableTimerNeeded
    • Lone stateless function -> StatelessFastPathPreferWorker
  • do_fit_score (line 132): Weighted sum of markers + structure bonuses (multi-function, deps, exported). Clamped to 0-100.

  • infer_do_annotation (line 165): Only emits annotation if score >= 60. Infers coordination_intent, routing_policy, and durability_policy.

Body Lowering (lib/lower.ml, 329 lines)

Rewrites AST nodes for distributed execution:

  • Map->KV rewriting (rewrite_map_call, line 24): store.get(key) -> await env.STORE.get(key), store.set(k,v) -> await env.STORE.put(k,v), store.delete(k) -> await env.STORE.delete(k).

  • Cross-group RPC rewriting (rewrite_service_call, line 61): getTask(id) -> await (await env.TASKS_NEXTID.fetch("http://internal/getTask", { method: "POST", body: JSON.stringify([id]) })).json(). Uses RawCode AST node since there is no ObjectExpression in the AST.

  • Full Worker script generation (generate_worker_script, line 206): Produces Env interface, async handler functions with rewritten bodies, and a fetch router that dispatches by pathname.

7. Test Coverage

9 test suites, all using raw assert (no test framework):

Test File Lines What It Tests
test_scope.ml 52 Module vars, closure free vars, captured mutable, called functions, exports
test_decompose.ml 30 Worker group formation, owned_state, service_deps
test_ir.ml 73 IR config: bindings, routes, script content
test_durable_object_hints.ml 33 DO fit scores, markers on storage/cache/api workers
test_generate.ml 27 Generated output strings (JSON, wrangler)
test_parse.ml 98 Integration: parsed kv-store.ts == hand-built fixture AST (needs bun)
test_emit.ml 115 AST -> TypeScript source for every node type
test_lower.ml 117 Map->KV rewriting, cross-service rewriting, extract_functions
test_task_api.ml 153 Full pipeline for task-api: scope, decompose, IR, generate

Two test fixtures in fixture.ml:

  • build_kv_store_ast() (line 47): KV store with cache (3 workers: storage, cache, handleRequest)
  • build_task_api_ast() (line 136): Task CRUD API (2 workers: tasks_nextId, handleRequest)

8. Summary: What's Built vs. What's Not

Fully implemented:

  • TypeScript ESTree subset parser (via external bun bridge)
  • Scope/closure analysis (free vars, mutable state detection, called functions, exports)
  • Closure-based decomposition into Worker groups
  • Mutable state -> KV binding translation
  • Cross-group function calls -> service binding + fetch RPC translation
  • Full body lowering (AST rewriting) with KV API calls and service RPCs
  • AST -> TypeScript emitter
  • IR with rich Durable Object annotations (markers, intents, routing, durability)
  • Heuristic DO fitness scoring
  • Wrangler TOML generation
  • Project file emission (--emit-project)
  • CI pipeline

Not yet implemented / gaps:

  • No custom parser -- relies entirely on external bun + @typescript-eslint/typescript-estree
  • AST is a minimal subset (19 node types) -- no support for classes, for/while loops, object literals, array literals, template literals, assignment expressions, ternary, try/catch, switch, destructuring, spread, etc.
  • No type system / type checker
  • The .cl examples (hello.cl, closure_worker.cl) suggest an original CloudLang syntax was planned but never implemented
  • No ObjectExpression in AST forces RawCode workaround in lowering
  • No DurableObject or R2Bucket or D1Database resources are ever emitted by the decompose pass -- only Worker resources
  • No error recovery or source locations in AST
  • Tests don't use a proper framework (just bare assert)
1,980in / 5,006out