@phuetz/code-buddy 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/.codebuddy/skills/bundled/brave-search/SKILL.md +490 -0
  2. package/.codebuddy/skills/bundled/exa-search/SKILL.md +1122 -0
  3. package/.codebuddy/skills/bundled/perplexity/SKILL.md +748 -0
  4. package/.codebuddy/skills/bundled/playwright/SKILL.md +520 -0
  5. package/.codebuddy/skills/bundled/puppeteer/SKILL.md +708 -0
  6. package/.codebuddy/skills/bundled/web-fetch/SKILL.md +1003 -0
  7. package/README.md +56 -0
  8. package/dist/agent/agent-state.d.ts +3 -3
  9. package/dist/agent/agent-state.js +6 -6
  10. package/dist/agent/agent-state.js.map +1 -1
  11. package/dist/agent/base-agent.d.ts +4 -4
  12. package/dist/agent/base-agent.js +22 -9
  13. package/dist/agent/base-agent.js.map +1 -1
  14. package/dist/agent/cache-trace.d.ts +56 -0
  15. package/dist/agent/cache-trace.js +98 -0
  16. package/dist/agent/cache-trace.js.map +1 -0
  17. package/dist/agent/codebuddy-agent.js +3 -2
  18. package/dist/agent/codebuddy-agent.js.map +1 -1
  19. package/dist/agent/execution/agent-executor.d.ts +4 -4
  20. package/dist/agent/execution/agent-executor.js +41 -7
  21. package/dist/agent/execution/agent-executor.js.map +1 -1
  22. package/dist/agent/facades/agent-context-facade.js +1 -3
  23. package/dist/agent/facades/agent-context-facade.js.map +1 -1
  24. package/dist/agent/facades/message-history-manager.js +14 -12
  25. package/dist/agent/facades/message-history-manager.js.map +1 -1
  26. package/dist/agent/facades/session-facade.d.ts +3 -3
  27. package/dist/agent/facades/session-facade.js +6 -6
  28. package/dist/agent/facades/session-facade.js.map +1 -1
  29. package/dist/agent/history-repair.d.ts +37 -0
  30. package/dist/agent/history-repair.js +124 -0
  31. package/dist/agent/history-repair.js.map +1 -0
  32. package/dist/agent/specialized/archive-agent.d.ts +3 -0
  33. package/dist/agent/specialized/archive-agent.js +71 -31
  34. package/dist/agent/specialized/archive-agent.js.map +1 -1
  35. package/dist/agent/specialized/security-review/agent.js +19 -8
  36. package/dist/agent/specialized/security-review/agent.js.map +1 -1
  37. package/dist/agent/tool-executor.js +5 -0
  38. package/dist/agent/tool-executor.js.map +1 -1
  39. package/dist/agent/turn-diff-tracker.d.ts +79 -0
  40. package/dist/agent/turn-diff-tracker.js +195 -0
  41. package/dist/agent/turn-diff-tracker.js.map +1 -0
  42. package/dist/checkpoints/checkpoint-versioning.js +78 -20
  43. package/dist/checkpoints/checkpoint-versioning.js.map +1 -1
  44. package/dist/cli/config-loader.js +2 -4
  45. package/dist/cli/config-loader.js.map +1 -1
  46. package/dist/commands/handlers/fcs-handlers.js +1 -1
  47. package/dist/commands/handlers/fcs-handlers.js.map +1 -1
  48. package/dist/commands/handlers/memory-handlers.js +2 -1
  49. package/dist/commands/handlers/memory-handlers.js.map +1 -1
  50. package/dist/commands/handlers/worktree-handlers.js +11 -0
  51. package/dist/commands/handlers/worktree-handlers.js.map +1 -1
  52. package/dist/commands/mcp.d.ts +1 -0
  53. package/dist/commands/mcp.js +66 -7
  54. package/dist/commands/mcp.js.map +1 -1
  55. package/dist/commands/pipeline.js +25 -13
  56. package/dist/commands/pipeline.js.map +1 -1
  57. package/dist/config/model-tools.d.ts +41 -0
  58. package/dist/config/model-tools.js +194 -0
  59. package/dist/config/model-tools.js.map +1 -0
  60. package/dist/context/context-manager-v2.d.ts +2 -1
  61. package/dist/context/context-manager-v2.js +34 -5
  62. package/dist/context/context-manager-v2.js.map +1 -1
  63. package/dist/daemon/daemon-manager.js +23 -19
  64. package/dist/daemon/daemon-manager.js.map +1 -1
  65. package/dist/database/database-manager.d.ts +4 -0
  66. package/dist/database/database-manager.js +16 -7
  67. package/dist/database/database-manager.js.map +1 -1
  68. package/dist/desktop-automation/nutjs-provider.js +89 -0
  69. package/dist/desktop-automation/nutjs-provider.js.map +1 -1
  70. package/dist/fcs/builtins.d.ts +2 -6
  71. package/dist/fcs/builtins.js +2 -568
  72. package/dist/fcs/builtins.js.map +1 -1
  73. package/dist/fcs/codebuddy-bindings.d.ts +3 -43
  74. package/dist/fcs/codebuddy-bindings.js +2 -606
  75. package/dist/fcs/codebuddy-bindings.js.map +1 -1
  76. package/dist/fcs/index.d.ts +2 -27
  77. package/dist/fcs/index.js +2 -53
  78. package/dist/fcs/index.js.map +1 -1
  79. package/dist/fcs/lexer.d.ts +2 -37
  80. package/dist/fcs/lexer.js +2 -459
  81. package/dist/fcs/lexer.js.map +1 -1
  82. package/dist/fcs/parser.d.ts +2 -68
  83. package/dist/fcs/parser.js +2 -893
  84. package/dist/fcs/parser.js.map +1 -1
  85. package/dist/fcs/runtime.d.ts +2 -59
  86. package/dist/fcs/runtime.js +2 -623
  87. package/dist/fcs/runtime.js.map +1 -1
  88. package/dist/fcs/script-registry.d.ts +3 -69
  89. package/dist/fcs/script-registry.js +2 -219
  90. package/dist/fcs/script-registry.js.map +1 -1
  91. package/dist/fcs/sync-bindings.d.ts +3 -101
  92. package/dist/fcs/sync-bindings.js +2 -410
  93. package/dist/fcs/sync-bindings.js.map +1 -1
  94. package/dist/fcs/types.d.ts +2 -285
  95. package/dist/fcs/types.js +2 -103
  96. package/dist/fcs/types.js.map +1 -1
  97. package/dist/hooks/use-input-handler.d.ts +1 -1
  98. package/dist/index.js +5 -2
  99. package/dist/index.js.map +1 -1
  100. package/dist/input/voice-control.js +11 -5
  101. package/dist/input/voice-control.js.map +1 -1
  102. package/dist/integrations/json-rpc/server.js +5 -5
  103. package/dist/integrations/json-rpc/server.js.map +1 -1
  104. package/dist/integrations/mcp/mcp-server.js +1 -1
  105. package/dist/integrations/mcp/mcp-server.js.map +1 -1
  106. package/dist/mcp/client.js +2 -1
  107. package/dist/mcp/client.js.map +1 -1
  108. package/dist/mcp/config.js +89 -5
  109. package/dist/mcp/config.js.map +1 -1
  110. package/dist/mcp/mcp-client.js +65 -14
  111. package/dist/mcp/mcp-client.js.map +1 -1
  112. package/dist/mcp/transports.d.ts +0 -1
  113. package/dist/mcp/transports.js +1 -5
  114. package/dist/mcp/transports.js.map +1 -1
  115. package/dist/mcp/types.d.ts +2 -0
  116. package/dist/persistence/session-lock.d.ts +42 -0
  117. package/dist/persistence/session-lock.js +165 -0
  118. package/dist/persistence/session-lock.js.map +1 -0
  119. package/dist/persistence/session-store.d.ts +18 -3
  120. package/dist/persistence/session-store.js +90 -21
  121. package/dist/persistence/session-store.js.map +1 -1
  122. package/dist/plugins/isolated-plugin-runner.d.ts +6 -0
  123. package/dist/plugins/isolated-plugin-runner.js +19 -1
  124. package/dist/plugins/isolated-plugin-runner.js.map +1 -1
  125. package/dist/providers/local-llm-provider.js +21 -4
  126. package/dist/providers/local-llm-provider.js.map +1 -1
  127. package/dist/sandbox/docker-sandbox.js +7 -4
  128. package/dist/sandbox/docker-sandbox.js.map +1 -1
  129. package/dist/scripting/builtins.d.ts +8 -3
  130. package/dist/scripting/builtins.js +506 -355
  131. package/dist/scripting/builtins.js.map +1 -1
  132. package/dist/scripting/codebuddy-bindings.d.ts +47 -0
  133. package/dist/scripting/codebuddy-bindings.js +487 -0
  134. package/dist/scripting/codebuddy-bindings.js.map +1 -0
  135. package/dist/scripting/index.d.ts +33 -30
  136. package/dist/scripting/index.js +41 -36
  137. package/dist/scripting/index.js.map +1 -1
  138. package/dist/scripting/lexer.d.ts +31 -13
  139. package/dist/scripting/lexer.js +379 -292
  140. package/dist/scripting/lexer.js.map +1 -1
  141. package/dist/scripting/parser.d.ts +63 -44
  142. package/dist/scripting/parser.js +700 -473
  143. package/dist/scripting/parser.js.map +1 -1
  144. package/dist/scripting/runtime.d.ts +55 -24
  145. package/dist/scripting/runtime.js +600 -288
  146. package/dist/scripting/runtime.js.map +1 -1
  147. package/dist/scripting/script-registry.d.ts +54 -0
  148. package/dist/scripting/script-registry.js +202 -0
  149. package/dist/scripting/script-registry.js.map +1 -0
  150. package/dist/scripting/sync-bindings.d.ts +105 -0
  151. package/dist/scripting/sync-bindings.js +353 -0
  152. package/dist/scripting/sync-bindings.js.map +1 -0
  153. package/dist/scripting/types.d.ts +297 -199
  154. package/dist/scripting/types.js +86 -60
  155. package/dist/scripting/types.js.map +1 -1
  156. package/dist/search/usearch-index.js +42 -7
  157. package/dist/search/usearch-index.js.map +1 -1
  158. package/dist/security/bash-parser.d.ts +51 -0
  159. package/dist/security/bash-parser.js +327 -0
  160. package/dist/security/bash-parser.js.map +1 -0
  161. package/dist/security/skill-scanner.d.ts +36 -0
  162. package/dist/security/skill-scanner.js +149 -0
  163. package/dist/security/skill-scanner.js.map +1 -0
  164. package/dist/security/trust-folders.d.ts +1 -0
  165. package/dist/security/trust-folders.js +19 -1
  166. package/dist/security/trust-folders.js.map +1 -1
  167. package/dist/server/websocket/handler.js +15 -5
  168. package/dist/server/websocket/handler.js.map +1 -1
  169. package/dist/skills/eligibility.js +26 -4
  170. package/dist/skills/eligibility.js.map +1 -1
  171. package/dist/tasks/background-tasks.js +5 -1
  172. package/dist/tasks/background-tasks.js.map +1 -1
  173. package/dist/tools/apply-patch.d.ts +55 -0
  174. package/dist/tools/apply-patch.js +273 -0
  175. package/dist/tools/apply-patch.js.map +1 -0
  176. package/dist/tools/registry/bash-tools.js +6 -3
  177. package/dist/tools/registry/bash-tools.js.map +1 -1
  178. package/dist/tools/registry/misc-tools.js +1 -2
  179. package/dist/tools/registry/misc-tools.js.map +1 -1
  180. package/dist/tools/registry/search-tools.js +1 -1
  181. package/dist/tools/registry/search-tools.js.map +1 -1
  182. package/dist/tools/registry/text-editor-tools.js +1 -1
  183. package/dist/tools/registry/text-editor-tools.js.map +1 -1
  184. package/dist/tools/registry/todo-tools.js +37 -5
  185. package/dist/tools/registry/todo-tools.js.map +1 -1
  186. package/dist/tools/registry/tool-registry.js +5 -4
  187. package/dist/tools/registry/tool-registry.js.map +1 -1
  188. package/dist/tools/registry/web-tools.d.ts +1 -1
  189. package/dist/tools/registry/web-tools.js +28 -8
  190. package/dist/tools/registry/web-tools.js.map +1 -1
  191. package/dist/tools/text-editor.d.ts +1 -1
  192. package/dist/tools/text-editor.js +23 -5
  193. package/dist/tools/text-editor.js.map +1 -1
  194. package/dist/tools/web-search.d.ts +52 -37
  195. package/dist/tools/web-search.js +368 -163
  196. package/dist/tools/web-search.js.map +1 -1
  197. package/dist/ui/components/ChatInterface.d.ts +1 -1
  198. package/dist/utils/head-tail-truncation.d.ts +34 -0
  199. package/dist/utils/head-tail-truncation.js +98 -0
  200. package/dist/utils/head-tail-truncation.js.map +1 -0
  201. package/dist/utils/sanitize.d.ts +5 -0
  202. package/dist/utils/sanitize.js +19 -0
  203. package/dist/utils/sanitize.js.map +1 -1
  204. package/dist/utils/settings-manager.js +4 -4
  205. package/dist/utils/settings-manager.js.map +1 -1
  206. package/package.json +3 -1
@@ -1,18 +1,27 @@
1
1
  /**
2
- * Buddy Script Parser
2
+ * Unified Script Parser
3
3
  *
4
- * Parses tokens into an Abstract Syntax Tree (AST)
4
+ * Based on FCS parser (superset) with Buddy Script additions:
5
+ * - AwaitExpression in unary parsing
6
+ * - ForCStyle statement (C-style for loops)
7
+ * - Export keyword
8
+ * - Question token for ternary (instead of checking Colon value)
9
+ * - Arrow functions / lambdas
10
+ *
11
+ * Extensions: .bs (primary), .fcs (backward-compatible alias)
5
12
  */
6
- import { TokenType } from './types.js';
13
+ import { TokenType, } from './types.js';
14
+ import { tokenize as lexerTokenize } from './lexer.js';
7
15
  import { createLoopGuard, LoopTimeoutError } from '../utils/errors.js';
8
16
  /** Maximum recursion depth for recursive descent parsing */
9
17
  const MAX_RECURSION_DEPTH = 500;
10
- export class Parser {
18
+ export class FCSParser {
11
19
  tokens;
12
20
  current = 0;
13
21
  recursionDepth = 0;
14
22
  constructor(tokens) {
15
- this.tokens = tokens;
23
+ // Filter out comments
24
+ this.tokens = tokens.filter(t => t.type !== TokenType.Comment);
16
25
  }
17
26
  /**
18
27
  * Track recursion depth to prevent stack overflow on deeply nested input
@@ -27,37 +36,59 @@ export class Parser {
27
36
  this.recursionDepth--;
28
37
  }
29
38
  parse() {
30
- const body = [];
39
+ const statements = [];
31
40
  const guard = createLoopGuard({
32
41
  maxIterations: 100000,
33
- context: 'BuddyScript program parsing',
42
+ context: 'program parsing',
34
43
  });
35
44
  while (!this.isAtEnd()) {
36
45
  guard();
37
- const stmt = this.declaration();
46
+ this.skipNewlines();
47
+ if (this.isAtEnd())
48
+ break;
49
+ const stmt = this.parseDeclaration();
38
50
  if (stmt) {
39
- body.push(stmt);
51
+ statements.push(stmt);
40
52
  }
53
+ this.skipNewlines();
41
54
  }
42
- return { type: 'Program', body };
55
+ return { type: 'Program', statements };
43
56
  }
44
- declaration() {
45
- this.enterRecursion('declaration');
57
+ // ============================================
58
+ // Statement Parsing
59
+ // ============================================
60
+ parseDeclaration() {
61
+ this.enterRecursion('parseDeclaration');
46
62
  try {
47
- if (this.match(TokenType.LET, TokenType.CONST)) {
48
- return this.variableDeclaration();
63
+ // Check for decorators
64
+ const decoratorGuard = createLoopGuard({
65
+ maxIterations: 1000,
66
+ context: 'decorator parsing',
67
+ });
68
+ while (this.match(TokenType.Decorator)) {
69
+ decoratorGuard();
70
+ // Decorators stored but not used yet
71
+ void this.previous().value;
72
+ }
73
+ if (this.matchKeyword('func', 'function') || this.matchKeyword('async')) {
74
+ return this.parseFunctionDeclaration();
75
+ }
76
+ if (this.matchKeyword('class')) {
77
+ return this.parseClassDeclaration();
49
78
  }
50
- if (this.check(TokenType.ASYNC) && this.checkNext(TokenType.FUNCTION)) {
51
- this.advance(); // async
52
- return this.functionDeclaration(true);
79
+ if (this.matchKeyword('let', 'const', 'var')) {
80
+ return this.parseVarDeclaration();
53
81
  }
54
- if (this.match(TokenType.FUNCTION)) {
55
- return this.functionDeclaration(false);
82
+ if (this.matchKeyword('import')) {
83
+ return this.parseImportStatement();
56
84
  }
57
- if (this.match(TokenType.IMPORT)) {
58
- return this.importStatement();
85
+ if (this.matchKeyword('export')) {
86
+ return this.parseExportStatement();
59
87
  }
60
- return this.statement();
88
+ if (this.matchKeyword('test')) {
89
+ return this.parseTestDeclaration();
90
+ }
91
+ return this.parseStatement();
61
92
  }
62
93
  catch (error) {
63
94
  // Re-throw recursion/loop errors without synchronizing
@@ -71,472 +102,597 @@ export class Parser {
71
102
  this.exitRecursion();
72
103
  }
73
104
  }
74
- variableDeclaration() {
75
- const kind = this.previous().value === 'const' ? 'const' : 'let';
76
- const name = this.consume(TokenType.IDENTIFIER, 'Expected variable name').value;
77
- let init = null;
78
- if (this.match(TokenType.ASSIGN)) {
79
- init = this.expression();
80
- }
81
- this.consumeOptional(TokenType.SEMICOLON);
105
+ parseStatement() {
106
+ if (this.matchKeyword('if'))
107
+ return this.parseIfStatement();
108
+ if (this.matchKeyword('while'))
109
+ return this.parseWhileStatement();
110
+ if (this.matchKeyword('for'))
111
+ return this.parseForStatement();
112
+ if (this.matchKeyword('return'))
113
+ return this.parseReturnStatement();
114
+ if (this.matchKeyword('break'))
115
+ return this.parseBreakStatement();
116
+ if (this.matchKeyword('continue'))
117
+ return this.parseContinueStatement();
118
+ if (this.matchKeyword('try'))
119
+ return this.parseTryStatement();
120
+ if (this.matchKeyword('throw'))
121
+ return this.parseThrowStatement();
122
+ if (this.matchKeyword('assert'))
123
+ return this.parseAssertStatement();
124
+ if (this.match(TokenType.LeftBrace))
125
+ return this.parseBlockStatement();
126
+ return this.parseExpressionStatement();
127
+ }
128
+ parseFunctionDeclaration() {
129
+ const isAsync = this.previous().value === 'async';
130
+ if (isAsync) {
131
+ this.consumeKeyword('func', 'function');
132
+ }
133
+ const name = this.consume(TokenType.Identifier, "Expected function name").value;
134
+ this.consume(TokenType.LeftParen, "Expected '(' after function name");
135
+ const parameters = this.parseParameters();
136
+ this.consume(TokenType.RightParen, "Expected ')' after parameters");
137
+ let returnType;
138
+ if (this.match(TokenType.Colon)) {
139
+ returnType = this.consume(TokenType.Identifier, "Expected return type").value;
140
+ }
141
+ this.consume(TokenType.LeftBrace, "Expected '{' before function body");
142
+ const body = this.parseBlockStatement();
82
143
  return {
83
- type: 'VariableDeclaration',
84
- kind,
144
+ type: 'FunctionDeclaration',
85
145
  name,
86
- init,
146
+ parameters,
147
+ body,
148
+ isAsync,
149
+ returnType,
87
150
  };
88
151
  }
89
- functionDeclaration(isAsync) {
90
- const name = this.consume(TokenType.IDENTIFIER, 'Expected function name').value;
91
- this.consume(TokenType.LPAREN, "Expected '(' after function name");
92
- const params = [];
93
- if (!this.check(TokenType.RPAREN)) {
152
+ parseParameters() {
153
+ const parameters = [];
154
+ if (!this.check(TokenType.RightParen)) {
94
155
  do {
95
- const paramName = this.consume(TokenType.IDENTIFIER, 'Expected parameter name').value;
156
+ const name = this.consume(TokenType.Identifier, "Expected parameter name").value;
157
+ let type;
96
158
  let defaultValue;
97
- if (this.match(TokenType.ASSIGN)) {
98
- defaultValue = this.expression();
159
+ if (this.match(TokenType.Colon)) {
160
+ type = this.consume(TokenType.Identifier, "Expected parameter type").value;
161
+ }
162
+ if (this.match(TokenType.Assign)) {
163
+ defaultValue = this.parseExpression();
99
164
  }
100
- params.push({ name: paramName, defaultValue });
101
- } while (this.match(TokenType.COMMA));
165
+ parameters.push({ name, type, defaultValue });
166
+ } while (this.match(TokenType.Comma));
102
167
  }
103
- this.consume(TokenType.RPAREN, "Expected ')' after parameters");
104
- this.consume(TokenType.LBRACE, "Expected '{' before function body");
105
- const body = this.block();
168
+ return parameters;
169
+ }
170
+ parseVarDeclaration() {
171
+ const keyword = this.previous().value;
172
+ const isConst = keyword === 'const';
173
+ const name = this.consume(TokenType.Identifier, "Expected variable name").value;
174
+ let varType;
175
+ if (this.match(TokenType.Colon)) {
176
+ varType = this.consume(TokenType.Identifier, "Expected type").value;
177
+ }
178
+ let initializer = null;
179
+ if (this.match(TokenType.Assign)) {
180
+ initializer = this.parseExpression();
181
+ }
182
+ else if (isConst) {
183
+ throw new Error("Const variable must be initialized");
184
+ }
185
+ this.consumeStatementEnd();
106
186
  return {
107
- type: 'FunctionDeclaration',
187
+ type: 'VarDeclaration',
108
188
  name,
109
- params,
110
- body,
111
- async: isAsync,
189
+ initializer,
190
+ isConst,
191
+ varType,
112
192
  };
113
193
  }
114
- importStatement() {
115
- const module = this.consume(TokenType.IDENTIFIER, 'Expected module name').value;
116
- this.consumeOptional(TokenType.SEMICOLON);
194
+ parseClassDeclaration() {
195
+ const name = this.consume(TokenType.Identifier, "Expected class name").value;
196
+ let baseClass;
197
+ if (this.match(TokenType.Colon)) {
198
+ baseClass = this.consume(TokenType.Identifier, "Expected base class name").value;
199
+ }
200
+ this.consume(TokenType.LeftBrace, "Expected '{' before class body");
201
+ const members = [];
202
+ const guard = createLoopGuard({
203
+ maxIterations: 10000,
204
+ context: 'class body parsing',
205
+ });
206
+ while (!this.check(TokenType.RightBrace) && !this.isAtEnd()) {
207
+ guard();
208
+ this.skipNewlines();
209
+ if (this.check(TokenType.RightBrace))
210
+ break;
211
+ if (this.matchKeyword('func', 'function', 'async')) {
212
+ members.push(this.parseFunctionDeclaration());
213
+ }
214
+ else if (this.matchKeyword('let', 'const', 'var')) {
215
+ members.push(this.parseVarDeclaration());
216
+ }
217
+ else {
218
+ // Skip unexpected tokens in class body to prevent infinite loop
219
+ this.advance();
220
+ }
221
+ this.skipNewlines();
222
+ }
223
+ this.consume(TokenType.RightBrace, "Expected '}' after class body");
117
224
  return {
118
- type: 'ImportStatement',
119
- module,
225
+ type: 'ClassDeclaration',
226
+ name,
227
+ baseClass,
228
+ members,
120
229
  };
121
230
  }
122
- statement() {
123
- if (this.match(TokenType.IF))
124
- return this.ifStatement();
125
- if (this.match(TokenType.WHILE))
126
- return this.whileStatement();
127
- if (this.match(TokenType.FOR))
128
- return this.forStatement();
129
- if (this.match(TokenType.RETURN))
130
- return this.returnStatement();
131
- if (this.match(TokenType.TRY))
132
- return this.tryStatement();
133
- if (this.match(TokenType.THROW))
134
- return this.throwStatement();
135
- if (this.match(TokenType.BREAK))
136
- return this.breakStatement();
137
- if (this.match(TokenType.CONTINUE))
138
- return this.continueStatement();
139
- if (this.match(TokenType.LBRACE))
140
- return this.block();
141
- return this.expressionStatement();
142
- }
143
- ifStatement() {
144
- this.consume(TokenType.LPAREN, "Expected '(' after 'if'");
145
- const condition = this.expression();
146
- this.consume(TokenType.RPAREN, "Expected ')' after condition");
147
- let consequent;
148
- if (this.match(TokenType.LBRACE)) {
149
- consequent = this.block();
150
- }
151
- else {
152
- consequent = {
153
- type: 'BlockStatement',
154
- body: [this.statement()],
155
- };
156
- }
157
- let alternate = null;
158
- if (this.match(TokenType.ELSE)) {
159
- if (this.match(TokenType.IF)) {
160
- alternate = this.ifStatement();
161
- }
162
- else if (this.match(TokenType.LBRACE)) {
163
- alternate = this.block();
231
+ parseIfStatement() {
232
+ // Optional parentheses
233
+ const hasParen = this.match(TokenType.LeftParen);
234
+ const condition = this.parseExpression();
235
+ if (hasParen) {
236
+ this.consume(TokenType.RightParen, "Expected ')' after condition");
237
+ }
238
+ const thenBranch = this.match(TokenType.LeftBrace)
239
+ ? this.parseBlockStatement()
240
+ : this.parseStatement();
241
+ let elseBranch = null;
242
+ if (this.matchKeyword('else')) {
243
+ if (this.matchKeyword('if')) {
244
+ elseBranch = this.parseIfStatement();
164
245
  }
165
246
  else {
166
- alternate = {
167
- type: 'BlockStatement',
168
- body: [this.statement()],
169
- };
247
+ elseBranch = this.match(TokenType.LeftBrace)
248
+ ? this.parseBlockStatement()
249
+ : this.parseStatement();
170
250
  }
171
251
  }
172
252
  return {
173
- type: 'IfStatement',
253
+ type: 'If',
174
254
  condition,
175
- consequent,
176
- alternate,
255
+ thenBranch,
256
+ elseBranch,
177
257
  };
178
258
  }
179
- whileStatement() {
180
- this.consume(TokenType.LPAREN, "Expected '(' after 'while'");
181
- const condition = this.expression();
182
- this.consume(TokenType.RPAREN, "Expected ')' after condition");
183
- this.consume(TokenType.LBRACE, "Expected '{' before while body");
184
- const body = this.block();
259
+ parseWhileStatement() {
260
+ const hasParen = this.match(TokenType.LeftParen);
261
+ const condition = this.parseExpression();
262
+ if (hasParen) {
263
+ this.consume(TokenType.RightParen, "Expected ')' after condition");
264
+ }
265
+ const body = this.match(TokenType.LeftBrace)
266
+ ? this.parseBlockStatement()
267
+ : this.parseStatement();
185
268
  return {
186
- type: 'WhileStatement',
269
+ type: 'While',
187
270
  condition,
188
271
  body,
189
272
  };
190
273
  }
191
- forStatement() {
192
- // Check if it's a C-style for loop: for (init; test; update) { }
193
- if (this.match(TokenType.LPAREN)) {
194
- return this.forCStyleStatement();
195
- }
196
- // Otherwise, for-in loop: for x in iterable { }
197
- const variable = this.consume(TokenType.IDENTIFIER, 'Expected variable name').value;
198
- this.consume(TokenType.IN, "Expected 'in' in for loop");
199
- const iterable = this.expression();
200
- this.consume(TokenType.LBRACE, "Expected '{' before for body");
201
- const body = this.block();
274
+ parseForStatement() {
275
+ // Check for C-style for loop: for (init; test; update) { }
276
+ if (this.match(TokenType.LeftParen)) {
277
+ return this.parseForCStyleStatement();
278
+ }
279
+ // For-in loop: for x in iterable { }
280
+ const variable = this.consume(TokenType.Identifier, "Expected variable name").value;
281
+ this.consumeKeyword('in');
282
+ const iterable = this.parseExpression();
283
+ const body = this.match(TokenType.LeftBrace)
284
+ ? this.parseBlockStatement()
285
+ : this.parseStatement();
202
286
  return {
203
- type: 'ForInStatement',
287
+ type: 'For',
204
288
  variable,
205
289
  iterable,
206
290
  body,
207
291
  };
208
292
  }
209
- forCStyleStatement() {
210
- // Parse C-style for: for (init; test; update) { }
293
+ parseForCStyleStatement() {
211
294
  // Parse init
212
295
  let init = null;
213
- if (!this.check(TokenType.SEMICOLON)) {
214
- if (this.match(TokenType.LET) || this.match(TokenType.CONST)) {
215
- const kind = this.previous().type === TokenType.LET ? 'let' : 'const';
216
- const name = this.consume(TokenType.IDENTIFIER, 'Expected variable name').value;
217
- let initValue = null;
218
- if (this.match(TokenType.ASSIGN)) {
219
- initValue = this.expression();
220
- }
221
- init = {
222
- type: 'VariableDeclaration',
223
- kind,
224
- name,
225
- init: initValue,
226
- };
296
+ if (!this.check(TokenType.Semicolon)) {
297
+ if (this.matchKeyword('let', 'const', 'var')) {
298
+ init = this.parseVarDeclaration();
227
299
  }
228
300
  else {
229
- init = this.expression();
301
+ init = this.parseExpression();
302
+ this.consume(TokenType.Semicolon, "Expected ';' after for init");
230
303
  }
231
304
  }
232
- this.consume(TokenType.SEMICOLON, "Expected ';' after for init");
305
+ else {
306
+ this.advance(); // skip ;
307
+ }
308
+ // If parseVarDeclaration already consumed the semicolon, skip
233
309
  // Parse test
234
310
  let test = null;
235
- if (!this.check(TokenType.SEMICOLON)) {
236
- test = this.expression();
311
+ if (!this.check(TokenType.Semicolon)) {
312
+ test = this.parseExpression();
237
313
  }
238
- this.consume(TokenType.SEMICOLON, "Expected ';' after for condition");
314
+ this.consume(TokenType.Semicolon, "Expected ';' after for condition");
239
315
  // Parse update
240
316
  let update = null;
241
- if (!this.check(TokenType.RPAREN)) {
242
- update = this.expression();
317
+ if (!this.check(TokenType.RightParen)) {
318
+ update = this.parseExpression();
243
319
  }
244
- this.consume(TokenType.RPAREN, "Expected ')' after for clauses");
320
+ this.consume(TokenType.RightParen, "Expected ')' after for clauses");
245
321
  // Parse body
246
- this.consume(TokenType.LBRACE, "Expected '{' before for body");
247
- const body = this.block();
322
+ const body = this.match(TokenType.LeftBrace)
323
+ ? this.parseBlockStatement()
324
+ : this.parseStatement();
248
325
  return {
249
- type: 'ForStatement',
326
+ type: 'ForCStyle',
250
327
  init,
251
328
  test,
252
329
  update,
253
330
  body,
254
331
  };
255
332
  }
256
- returnStatement() {
257
- let argument = null;
258
- if (!this.check(TokenType.SEMICOLON) && !this.check(TokenType.RBRACE) && !this.check(TokenType.EOF)) {
259
- argument = this.expression();
333
+ parseBlockStatement() {
334
+ const statements = [];
335
+ const guard = createLoopGuard({
336
+ maxIterations: 100000,
337
+ context: 'block statement parsing',
338
+ });
339
+ while (!this.check(TokenType.RightBrace) && !this.isAtEnd()) {
340
+ guard();
341
+ this.skipNewlines();
342
+ if (this.check(TokenType.RightBrace))
343
+ break;
344
+ const stmt = this.parseDeclaration();
345
+ if (stmt) {
346
+ statements.push(stmt);
347
+ }
348
+ this.skipNewlines();
349
+ }
350
+ this.consume(TokenType.RightBrace, "Expected '}' after block");
351
+ return { type: 'Block', statements };
352
+ }
353
+ parseExpressionStatement() {
354
+ const expression = this.parseExpression();
355
+ this.consumeStatementEnd();
356
+ return { type: 'ExpressionStmt', expression };
357
+ }
358
+ parseReturnStatement() {
359
+ let value = null;
360
+ if (!this.check(TokenType.Newline) && !this.check(TokenType.Semicolon) && !this.check(TokenType.RightBrace)) {
361
+ value = this.parseExpression();
362
+ }
363
+ this.consumeStatementEnd();
364
+ return { type: 'Return', value };
365
+ }
366
+ parseBreakStatement() {
367
+ this.consumeStatementEnd();
368
+ return { type: 'Break' };
369
+ }
370
+ parseContinueStatement() {
371
+ this.consumeStatementEnd();
372
+ return { type: 'Continue' };
373
+ }
374
+ parseTryStatement() {
375
+ this.consume(TokenType.LeftBrace, "Expected '{' after 'try'");
376
+ const tryBlock = this.parseBlockStatement();
377
+ const catchClauses = [];
378
+ const catchGuard = createLoopGuard({
379
+ maxIterations: 1000,
380
+ context: 'catch clause parsing',
381
+ });
382
+ while (this.matchKeyword('catch')) {
383
+ catchGuard();
384
+ let variable;
385
+ let type;
386
+ if (this.match(TokenType.LeftParen)) {
387
+ if (this.match(TokenType.Identifier)) {
388
+ variable = this.previous().value;
389
+ if (this.match(TokenType.Colon)) {
390
+ type = this.consume(TokenType.Identifier, "Expected exception type").value;
391
+ }
392
+ }
393
+ this.consume(TokenType.RightParen, "Expected ')' after catch parameters");
394
+ }
395
+ this.consume(TokenType.LeftBrace, "Expected '{' after catch");
396
+ const body = this.parseBlockStatement();
397
+ catchClauses.push({ variable, type, body });
398
+ }
399
+ let finallyBlock = null;
400
+ if (this.matchKeyword('finally')) {
401
+ this.consume(TokenType.LeftBrace, "Expected '{' after 'finally'");
402
+ finallyBlock = this.parseBlockStatement();
260
403
  }
261
- this.consumeOptional(TokenType.SEMICOLON);
262
404
  return {
263
- type: 'ReturnStatement',
264
- argument,
405
+ type: 'Try',
406
+ tryBlock,
407
+ catchClauses,
408
+ finallyBlock,
265
409
  };
266
410
  }
267
- tryStatement() {
268
- this.consume(TokenType.LBRACE, "Expected '{' after 'try'");
269
- const block = this.block();
270
- let handler = null;
271
- if (this.match(TokenType.CATCH)) {
272
- this.consume(TokenType.LPAREN, "Expected '(' after 'catch'");
273
- const param = this.consume(TokenType.IDENTIFIER, 'Expected error parameter').value;
274
- this.consume(TokenType.RPAREN, "Expected ')' after catch parameter");
275
- this.consume(TokenType.LBRACE, "Expected '{' before catch body");
276
- const catchBody = this.block();
277
- handler = { param, body: catchBody };
411
+ parseThrowStatement() {
412
+ const expression = this.parseExpression();
413
+ this.consumeStatementEnd();
414
+ return { type: 'Throw', expression };
415
+ }
416
+ parseImportStatement() {
417
+ const names = [];
418
+ let module = '';
419
+ let alias;
420
+ if (this.match(TokenType.LeftBrace)) {
421
+ // import { name1, name2 } from "module"
422
+ do {
423
+ names.push(this.consume(TokenType.Identifier, "Expected import name").value);
424
+ } while (this.match(TokenType.Comma));
425
+ this.consume(TokenType.RightBrace, "Expected '}' after import names");
426
+ this.consumeKeyword('from');
427
+ module = this.consume(TokenType.String, "Expected module name").value;
428
+ }
429
+ else if (this.match(TokenType.Multiply)) {
430
+ // import * as alias from "module"
431
+ this.consumeKeyword('as');
432
+ alias = this.consume(TokenType.Identifier, "Expected alias name").value;
433
+ this.consumeKeyword('from');
434
+ module = this.consume(TokenType.String, "Expected module name").value;
435
+ }
436
+ else if (this.check(TokenType.String)) {
437
+ // import "module"
438
+ module = this.consume(TokenType.String, "Expected module name").value;
439
+ }
440
+ else if (this.check(TokenType.Identifier)) {
441
+ // import name OR import name from "module"
442
+ const firstIdent = this.consume(TokenType.Identifier, "Expected import name").value;
443
+ if (this.checkKeyword('from')) {
444
+ names.push(firstIdent);
445
+ this.consumeKeyword('from');
446
+ module = this.consume(TokenType.String, "Expected module name").value;
447
+ }
448
+ else {
449
+ // Simple import like: import fc
450
+ module = firstIdent;
451
+ }
278
452
  }
453
+ this.consumeStatementEnd();
279
454
  return {
280
- type: 'TryStatement',
281
- block,
282
- handler,
455
+ type: 'Import',
456
+ module,
457
+ names,
458
+ alias,
283
459
  };
284
460
  }
285
- throwStatement() {
286
- const argument = this.expression();
287
- this.consumeOptional(TokenType.SEMICOLON);
461
+ parseExportStatement() {
462
+ // export let/const/function/class
463
+ const declaration = this.parseDeclaration();
464
+ if (!declaration) {
465
+ throw new Error("Expected declaration after 'export'");
466
+ }
288
467
  return {
289
- type: 'ThrowStatement',
290
- argument,
468
+ type: 'Export',
469
+ declaration,
291
470
  };
292
471
  }
293
- breakStatement() {
294
- this.consumeOptional(TokenType.SEMICOLON);
295
- return { type: 'BreakStatement' };
296
- }
297
- continueStatement() {
298
- this.consumeOptional(TokenType.SEMICOLON);
299
- return { type: 'ContinueStatement' };
300
- }
301
- block() {
302
- const statements = [];
303
- const guard = createLoopGuard({
304
- maxIterations: 100000,
305
- context: 'BuddyScript block parsing',
306
- });
307
- while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
308
- guard();
309
- const decl = this.declaration();
310
- if (decl)
311
- statements.push(decl);
472
+ parseTestDeclaration() {
473
+ const name = this.consume(TokenType.String, "Expected test name as string").value;
474
+ const tags = [];
475
+ if (this.match(TokenType.LeftBracket)) {
476
+ do {
477
+ if (this.match(TokenType.String)) {
478
+ tags.push(this.previous().value);
479
+ }
480
+ } while (this.match(TokenType.Comma));
481
+ this.consume(TokenType.RightBracket, "Expected ']' after tags");
312
482
  }
313
- this.consume(TokenType.RBRACE, "Expected '}' after block");
483
+ this.consume(TokenType.LeftBrace, "Expected '{' before test body");
484
+ const body = this.parseBlockStatement();
314
485
  return {
315
- type: 'BlockStatement',
316
- body: statements,
486
+ type: 'TestDeclaration',
487
+ name,
488
+ body,
489
+ tags,
317
490
  };
318
491
  }
319
- expressionStatement() {
320
- const expression = this.expression();
321
- this.consumeOptional(TokenType.SEMICOLON);
492
+ parseAssertStatement() {
493
+ const condition = this.parseExpression();
494
+ let message;
495
+ if (this.match(TokenType.Comma)) {
496
+ if (this.match(TokenType.String)) {
497
+ message = this.previous().value;
498
+ }
499
+ }
500
+ this.consumeStatementEnd();
322
501
  return {
323
- type: 'ExpressionStatement',
324
- expression,
502
+ type: 'Assert',
503
+ condition,
504
+ message,
325
505
  };
326
506
  }
327
507
  // ============================================
328
- // Expression Parsing (Precedence Climbing)
508
+ // Expression Parsing
329
509
  // ============================================
330
- expression() {
331
- return this.assignment();
332
- }
333
- assignment() {
334
- const expr = this.ternary();
335
- if (this.match(TokenType.ASSIGN, TokenType.PLUS_ASSIGN, TokenType.MINUS_ASSIGN)) {
336
- const operator = this.previous().value;
337
- const value = this.assignment();
338
- if (expr.type === 'Identifier' || expr.type === 'MemberExpression') {
339
- return {
340
- type: 'AssignmentExpression',
341
- operator,
342
- left: expr,
343
- right: value,
344
- };
345
- }
346
- throw new Error('Invalid assignment target');
510
+ parseExpression() {
511
+ return this.parseAssignment();
512
+ }
513
+ parseAssignment() {
514
+ const expr = this.parseTernary();
515
+ if (this.match(TokenType.Assign, TokenType.PlusAssign, TokenType.MinusAssign, TokenType.MultiplyAssign, TokenType.DivideAssign)) {
516
+ const operator = this.previous().type;
517
+ const value = this.parseAssignment();
518
+ return {
519
+ type: 'Assignment',
520
+ target: expr,
521
+ operator,
522
+ value,
523
+ };
347
524
  }
348
525
  return expr;
349
526
  }
350
- ternary() {
351
- let expr = this.or();
352
- if (this.match(TokenType.QUESTION)) {
353
- const consequent = this.expression();
354
- this.consume(TokenType.COLON, "Expected ':' in ternary expression");
355
- const alternate = this.ternary();
527
+ parseTernary() {
528
+ let expr = this.parsePipeline();
529
+ // Use dedicated Question token
530
+ if (this.match(TokenType.Question)) {
531
+ const consequent = this.parseExpression();
532
+ this.consume(TokenType.Colon, "Expected ':' in ternary expression");
533
+ const alternate = this.parseTernary();
356
534
  return {
357
- type: 'ConditionalExpression',
358
- test: expr,
535
+ type: 'Ternary',
536
+ condition: expr,
359
537
  consequent,
360
538
  alternate,
361
539
  };
362
540
  }
363
541
  return expr;
364
542
  }
365
- or() {
366
- let expr = this.and();
543
+ parsePipeline() {
544
+ let expr = this.parseLogicalOr();
367
545
  const guard = createLoopGuard({
368
546
  maxIterations: 10000,
369
- context: 'BuddyScript OR expression parsing',
547
+ context: 'pipeline expression parsing',
370
548
  });
371
- while (this.match(TokenType.OR)) {
549
+ while (this.match(TokenType.Pipeline)) {
372
550
  guard();
373
- const right = this.and();
374
- expr = {
375
- type: 'LogicalExpression',
376
- operator: '||',
377
- left: expr,
378
- right,
379
- };
551
+ const right = this.parseLogicalOr();
552
+ // Transform pipeline: expr |> func => func(expr)
553
+ if (right.type === 'Call') {
554
+ const call = right;
555
+ call.arguments.unshift(expr);
556
+ expr = call;
557
+ }
558
+ else if (right.type === 'Identifier') {
559
+ expr = {
560
+ type: 'Call',
561
+ callee: right,
562
+ arguments: [expr],
563
+ };
564
+ }
565
+ else {
566
+ throw new Error("Invalid pipeline target");
567
+ }
380
568
  }
381
569
  return expr;
382
570
  }
383
- and() {
384
- let expr = this.equality();
571
+ parseLogicalOr() {
572
+ let expr = this.parseLogicalAnd();
385
573
  const guard = createLoopGuard({
386
574
  maxIterations: 10000,
387
- context: 'BuddyScript AND expression parsing',
575
+ context: 'logical OR expression parsing',
388
576
  });
389
- while (this.match(TokenType.AND)) {
577
+ while (this.match(TokenType.Or)) {
390
578
  guard();
391
- const right = this.equality();
392
- expr = {
393
- type: 'LogicalExpression',
394
- operator: '&&',
395
- left: expr,
396
- right,
397
- };
579
+ const operator = this.previous().type;
580
+ const right = this.parseLogicalAnd();
581
+ expr = { type: 'Binary', left: expr, operator, right };
398
582
  }
399
583
  return expr;
400
584
  }
401
- equality() {
402
- let expr = this.comparison();
585
+ parseLogicalAnd() {
586
+ let expr = this.parseEquality();
403
587
  const guard = createLoopGuard({
404
588
  maxIterations: 10000,
405
- context: 'BuddyScript equality expression parsing',
589
+ context: 'logical AND expression parsing',
406
590
  });
407
- while (this.match(TokenType.EQUALS, TokenType.NOT_EQUALS)) {
591
+ while (this.match(TokenType.And)) {
408
592
  guard();
409
- const operator = this.previous().value;
410
- const right = this.comparison();
411
- expr = {
412
- type: 'BinaryExpression',
413
- operator,
414
- left: expr,
415
- right,
416
- };
593
+ const operator = this.previous().type;
594
+ const right = this.parseEquality();
595
+ expr = { type: 'Binary', left: expr, operator, right };
417
596
  }
418
597
  return expr;
419
598
  }
420
- comparison() {
421
- let expr = this.term();
599
+ parseEquality() {
600
+ let expr = this.parseComparison();
422
601
  const guard = createLoopGuard({
423
602
  maxIterations: 10000,
424
- context: 'BuddyScript comparison expression parsing',
603
+ context: 'equality expression parsing',
425
604
  });
426
- while (this.match(TokenType.LESS_THAN, TokenType.LESS_EQUAL, TokenType.GREATER_THAN, TokenType.GREATER_EQUAL)) {
605
+ while (this.match(TokenType.Equal, TokenType.NotEqual)) {
427
606
  guard();
428
- const operator = this.previous().value;
429
- const right = this.term();
430
- expr = {
431
- type: 'BinaryExpression',
432
- operator,
433
- left: expr,
434
- right,
435
- };
607
+ const operator = this.previous().type;
608
+ const right = this.parseComparison();
609
+ expr = { type: 'Binary', left: expr, operator, right };
436
610
  }
437
611
  return expr;
438
612
  }
439
- term() {
440
- let expr = this.factor();
613
+ parseComparison() {
614
+ let expr = this.parseAddition();
441
615
  const guard = createLoopGuard({
442
616
  maxIterations: 10000,
443
- context: 'BuddyScript term expression parsing',
617
+ context: 'comparison expression parsing',
444
618
  });
445
- while (this.match(TokenType.PLUS, TokenType.MINUS)) {
619
+ while (this.match(TokenType.Less, TokenType.Greater, TokenType.LessEqual, TokenType.GreaterEqual)) {
446
620
  guard();
447
- const operator = this.previous().value;
448
- const right = this.factor();
449
- expr = {
450
- type: 'BinaryExpression',
451
- operator,
452
- left: expr,
453
- right,
454
- };
621
+ const operator = this.previous().type;
622
+ const right = this.parseAddition();
623
+ expr = { type: 'Binary', left: expr, operator, right };
455
624
  }
456
625
  return expr;
457
626
  }
458
- factor() {
459
- let expr = this.power();
627
+ parseAddition() {
628
+ let expr = this.parseMultiplication();
460
629
  const guard = createLoopGuard({
461
630
  maxIterations: 10000,
462
- context: 'BuddyScript factor expression parsing',
631
+ context: 'addition expression parsing',
463
632
  });
464
- while (this.match(TokenType.MULTIPLY, TokenType.DIVIDE, TokenType.MODULO)) {
633
+ while (this.match(TokenType.Plus, TokenType.Minus)) {
465
634
  guard();
466
- const operator = this.previous().value;
467
- const right = this.power();
468
- expr = {
469
- type: 'BinaryExpression',
470
- operator,
471
- left: expr,
472
- right,
473
- };
635
+ const operator = this.previous().type;
636
+ const right = this.parseMultiplication();
637
+ expr = { type: 'Binary', left: expr, operator, right };
474
638
  }
475
639
  return expr;
476
640
  }
477
- power() {
478
- let expr = this.unary();
479
- if (this.match(TokenType.POWER)) {
480
- const right = this.power(); // Right associative
481
- expr = {
482
- type: 'BinaryExpression',
483
- operator: '**',
484
- left: expr,
485
- right,
486
- };
641
+ parseMultiplication() {
642
+ let expr = this.parsePower();
643
+ const guard = createLoopGuard({
644
+ maxIterations: 10000,
645
+ context: 'multiplication expression parsing',
646
+ });
647
+ while (this.match(TokenType.Multiply, TokenType.Divide, TokenType.Modulo)) {
648
+ guard();
649
+ const operator = this.previous().type;
650
+ const right = this.parsePower();
651
+ expr = { type: 'Binary', left: expr, operator, right };
487
652
  }
488
653
  return expr;
489
654
  }
490
- unary() {
491
- if (this.match(TokenType.NOT, TokenType.MINUS)) {
492
- const operator = this.previous().value;
493
- const right = this.unary();
494
- return {
495
- type: 'UnaryExpression',
496
- operator,
497
- argument: right,
498
- prefix: true,
499
- };
655
+ parsePower() {
656
+ let expr = this.parseUnary();
657
+ if (this.match(TokenType.Power)) {
658
+ const right = this.parsePower(); // Right associative
659
+ expr = { type: 'Binary', left: expr, operator: TokenType.Power, right };
500
660
  }
501
- if (this.match(TokenType.AWAIT)) {
502
- const argument = this.unary();
503
- return {
504
- type: 'AwaitExpression',
505
- argument,
506
- };
661
+ return expr;
662
+ }
663
+ parseUnary() {
664
+ if (this.match(TokenType.Not, TokenType.Minus)) {
665
+ const operator = this.previous().type;
666
+ const operand = this.parseUnary();
667
+ return { type: 'Unary', operator, operand };
507
668
  }
508
- return this.call();
669
+ // Await expression (from Buddy Script)
670
+ if (this.matchKeyword('await')) {
671
+ const argument = this.parseUnary();
672
+ return { type: 'Await', argument };
673
+ }
674
+ return this.parsePostfix();
509
675
  }
510
- call() {
511
- let expr = this.primary();
512
- // Guard against infinite loops in call/member parsing (deeply nested chains)
676
+ parsePostfix() {
677
+ let expr = this.parsePrimary();
678
+ // Guard against infinite loops in postfix parsing
513
679
  const guard = createLoopGuard({
514
680
  maxIterations: 10000,
515
- context: 'BuddyScript call expression parsing',
681
+ context: 'postfix expression parsing',
516
682
  });
517
683
  while (true) {
518
684
  guard();
519
- if (this.match(TokenType.LPAREN)) {
520
- expr = this.finishCall(expr);
685
+ if (this.match(TokenType.LeftParen)) {
686
+ expr = this.parseCall(expr);
521
687
  }
522
- else if (this.match(TokenType.DOT)) {
523
- const name = this.consume(TokenType.IDENTIFIER, 'Expected property name').value;
524
- expr = {
525
- type: 'MemberExpression',
526
- object: expr,
527
- property: { type: 'Identifier', name },
528
- computed: false,
529
- };
688
+ else if (this.match(TokenType.LeftBracket)) {
689
+ const index = this.parseExpression();
690
+ this.consume(TokenType.RightBracket, "Expected ']' after index");
691
+ expr = { type: 'Index', object: expr, index };
530
692
  }
531
- else if (this.match(TokenType.LBRACKET)) {
532
- const property = this.expression();
533
- this.consume(TokenType.RBRACKET, "Expected ']' after index");
534
- expr = {
535
- type: 'MemberExpression',
536
- object: expr,
537
- property,
538
- computed: true,
539
- };
693
+ else if (this.match(TokenType.Dot)) {
694
+ const member = this.consume(TokenType.Identifier, "Expected member name").value;
695
+ expr = { type: 'Member', object: expr, member, computed: false };
540
696
  }
541
697
  else {
542
698
  break;
@@ -544,139 +700,185 @@ export class Parser {
544
700
  }
545
701
  return expr;
546
702
  }
547
- finishCall(callee) {
703
+ parseCall(callee) {
548
704
  const args = [];
549
- if (!this.check(TokenType.RPAREN)) {
705
+ const namedArgs = {};
706
+ if (!this.check(TokenType.RightParen)) {
550
707
  do {
551
- // Handle named arguments: name: value
552
- if (this.check(TokenType.IDENTIFIER) && this.checkNext(TokenType.COLON)) {
708
+ // Check for named argument: name: value
709
+ if (this.check(TokenType.Identifier) && this.checkNext(TokenType.Colon)) {
553
710
  const name = this.advance().value;
554
711
  this.advance(); // :
555
- const value = this.expression();
556
- args.push({
557
- type: 'ObjectExpression',
558
- properties: [{ key: name, value, computed: false }],
559
- });
712
+ namedArgs[name] = this.parseExpression();
560
713
  }
561
714
  else {
562
- args.push(this.expression());
715
+ args.push(this.parseExpression());
563
716
  }
564
- } while (this.match(TokenType.COMMA));
717
+ } while (this.match(TokenType.Comma));
565
718
  }
566
- this.consume(TokenType.RPAREN, "Expected ')' after arguments");
567
- return {
568
- type: 'CallExpression',
719
+ this.consume(TokenType.RightParen, "Expected ')' after arguments");
720
+ const result = {
721
+ type: 'Call',
569
722
  callee,
570
723
  arguments: args,
571
724
  };
725
+ if (Object.keys(namedArgs).length > 0) {
726
+ result.namedArgs = namedArgs;
727
+ }
728
+ return result;
572
729
  }
573
- primary() {
574
- if (this.match(TokenType.NUMBER, TokenType.STRING, TokenType.BOOLEAN, TokenType.NULL)) {
730
+ parsePrimary() {
731
+ if (this.match(TokenType.Boolean)) {
575
732
  return {
576
733
  type: 'Literal',
577
- value: this.previous().value,
734
+ value: this.previous().value === 'true',
735
+ tokenType: TokenType.Boolean,
578
736
  };
579
737
  }
580
- if (this.match(TokenType.IDENTIFIER)) {
581
- return {
582
- type: 'Identifier',
583
- name: this.previous().value,
584
- };
738
+ if (this.match(TokenType.Null)) {
739
+ return { type: 'Literal', value: null, tokenType: TokenType.Null };
585
740
  }
586
- if (this.match(TokenType.LBRACKET)) {
587
- return this.arrayExpression();
741
+ if (this.match(TokenType.Number)) {
742
+ const value = this.previous().value;
743
+ const num = value.includes('.') ? parseFloat(value) : parseInt(value, 10);
744
+ return { type: 'Literal', value: num, tokenType: TokenType.Number };
588
745
  }
589
- if (this.match(TokenType.LBRACE)) {
590
- return this.objectExpression();
591
- }
592
- if (this.match(TokenType.LPAREN)) {
593
- // Could be grouping or arrow function
594
- const expr = this.expression();
595
- this.consume(TokenType.RPAREN, "Expected ')' after expression");
596
- if (this.match(TokenType.ARROW)) {
597
- return this.arrowFunction([expr]);
746
+ if (this.match(TokenType.String)) {
747
+ const value = this.previous().value;
748
+ // Check for interpolation
749
+ if (value.includes('${')) {
750
+ return this.parseStringInterpolation(value);
751
+ }
752
+ return { type: 'Literal', value, tokenType: TokenType.String };
753
+ }
754
+ if (this.match(TokenType.Identifier)) {
755
+ return { type: 'Identifier', name: this.previous().value };
756
+ }
757
+ if (this.match(TokenType.LeftParen)) {
758
+ // Check for lambda / arrow function
759
+ if (this.check(TokenType.Identifier) || this.check(TokenType.RightParen)) {
760
+ const checkpoint = this.current;
761
+ const parameters = [];
762
+ if (!this.check(TokenType.RightParen)) {
763
+ do {
764
+ if (this.match(TokenType.Identifier)) {
765
+ parameters.push(this.previous().value);
766
+ }
767
+ else {
768
+ this.current = checkpoint;
769
+ break;
770
+ }
771
+ } while (this.match(TokenType.Comma));
772
+ }
773
+ if (this.current !== checkpoint && this.match(TokenType.RightParen) && this.match(TokenType.Arrow)) {
774
+ const body = this.match(TokenType.LeftBrace)
775
+ ? this.parseBlockStatement()
776
+ : this.parseExpression();
777
+ return { type: 'Lambda', parameters, body };
778
+ }
779
+ this.current = checkpoint;
598
780
  }
781
+ const expr = this.parseExpression();
782
+ this.consume(TokenType.RightParen, "Expected ')' after expression");
599
783
  return expr;
600
784
  }
601
- throw new Error(`Unexpected token: ${this.peek().type} at line ${this.peek().line}`);
785
+ if (this.match(TokenType.LeftBracket)) {
786
+ return this.parseArrayLiteral();
787
+ }
788
+ if (this.match(TokenType.LeftBrace)) {
789
+ return this.parseDictLiteral();
790
+ }
791
+ throw new Error(`Unexpected token: ${this.peek().type} (${this.peek().value}) at line ${this.peek().line}`);
602
792
  }
603
- arrayExpression() {
793
+ parseStringInterpolation(value) {
794
+ const parts = [];
795
+ let current = 0;
796
+ const guard = createLoopGuard({
797
+ maxIterations: 10000,
798
+ context: 'string interpolation parsing',
799
+ });
800
+ while (current < value.length) {
801
+ guard();
802
+ const start = value.indexOf('${', current);
803
+ if (start === -1) {
804
+ if (current < value.length) {
805
+ parts.push({
806
+ type: 'Literal',
807
+ value: value.substring(current),
808
+ tokenType: TokenType.String,
809
+ });
810
+ }
811
+ break;
812
+ }
813
+ if (start > current) {
814
+ parts.push({
815
+ type: 'Literal',
816
+ value: value.substring(current, start),
817
+ tokenType: TokenType.String,
818
+ });
819
+ }
820
+ const end = value.indexOf('}', start + 2);
821
+ if (end === -1) {
822
+ throw new Error("Unterminated string interpolation");
823
+ }
824
+ const exprCode = value.substring(start + 2, end);
825
+ const tokens = lexerTokenize(exprCode);
826
+ const parser = new FCSParser(tokens);
827
+ parts.push(parser.parseExpression());
828
+ current = end + 1;
829
+ }
830
+ return { type: 'Interpolation', parts };
831
+ }
832
+ parseArrayLiteral() {
604
833
  const elements = [];
605
- if (!this.check(TokenType.RBRACKET)) {
834
+ if (!this.check(TokenType.RightBracket)) {
606
835
  do {
607
- elements.push(this.expression());
608
- } while (this.match(TokenType.COMMA));
836
+ elements.push(this.parseExpression());
837
+ } while (this.match(TokenType.Comma));
609
838
  }
610
- this.consume(TokenType.RBRACKET, "Expected ']' after array");
611
- return {
612
- type: 'ArrayExpression',
613
- elements,
614
- };
839
+ this.consume(TokenType.RightBracket, "Expected ']' after array elements");
840
+ return { type: 'Array', elements };
615
841
  }
616
- objectExpression() {
617
- const properties = [];
618
- if (!this.check(TokenType.RBRACE)) {
842
+ parseDictLiteral() {
843
+ const elements = new Map();
844
+ if (!this.check(TokenType.RightBrace)) {
619
845
  do {
620
846
  let key;
621
- let computed = false;
622
- if (this.match(TokenType.LBRACKET)) {
623
- key = this.expression();
624
- this.consume(TokenType.RBRACKET, "Expected ']' after computed property");
625
- computed = true;
847
+ if (this.match(TokenType.String)) {
848
+ key = this.previous().value;
626
849
  }
627
- else if (this.check(TokenType.STRING)) {
628
- key = this.advance().value;
850
+ else if (this.match(TokenType.Identifier)) {
851
+ key = this.previous().value;
629
852
  }
630
853
  else {
631
- key = this.consume(TokenType.IDENTIFIER, 'Expected property name').value;
854
+ throw new Error("Expected string or identifier for dict key");
632
855
  }
633
- this.consume(TokenType.COLON, "Expected ':' after property name");
634
- const value = this.expression();
635
- properties.push({ key, value, computed });
636
- } while (this.match(TokenType.COMMA));
637
- }
638
- this.consume(TokenType.RBRACE, "Expected '}' after object");
639
- return {
640
- type: 'ObjectExpression',
641
- properties,
642
- };
643
- }
644
- arrowFunction(params) {
645
- const paramNodes = params.map(p => {
646
- if (p.type !== 'Identifier') {
647
- throw new Error('Arrow function parameters must be identifiers');
648
- }
649
- return { name: p.name };
650
- });
651
- let body;
652
- if (this.match(TokenType.LBRACE)) {
653
- body = this.block();
856
+ this.consume(TokenType.Colon, "Expected ':' after dict key");
857
+ const value = this.parseExpression();
858
+ elements.set(key, value);
859
+ } while (this.match(TokenType.Comma));
654
860
  }
655
- else {
656
- body = this.expression();
657
- }
658
- return {
659
- type: 'ArrowFunction',
660
- params: paramNodes,
661
- body,
662
- async: false,
663
- };
861
+ this.consume(TokenType.RightBrace, "Expected '}' after dict elements");
862
+ return { type: 'Dict', elements };
664
863
  }
665
864
  // ============================================
666
- // Utility Methods
865
+ // Helper Methods
667
866
  // ============================================
668
- match(...types) {
669
- for (const type of types) {
670
- if (this.check(type)) {
671
- this.advance();
672
- return true;
673
- }
674
- }
675
- return false;
867
+ isAtEnd() {
868
+ return this.peek().type === TokenType.EOF;
869
+ }
870
+ peek() {
871
+ return this.tokens[Math.min(this.current, this.tokens.length - 1)];
872
+ }
873
+ previous() {
874
+ return this.tokens[this.current - 1];
875
+ }
876
+ advance() {
877
+ if (!this.isAtEnd())
878
+ this.current++;
879
+ return this.previous();
676
880
  }
677
881
  check(type) {
678
- if (this.isAtEnd())
679
- return false;
680
882
  return this.peek().type === type;
681
883
  }
682
884
  checkNext(type) {
@@ -684,58 +886,83 @@ export class Parser {
684
886
  return false;
685
887
  return this.tokens[this.current + 1].type === type;
686
888
  }
687
- advance() {
688
- if (!this.isAtEnd())
689
- this.current++;
690
- return this.previous();
691
- }
692
- isAtEnd() {
693
- return this.peek().type === TokenType.EOF;
889
+ checkKeyword(...keywords) {
890
+ if (this.peek().type !== TokenType.Keyword)
891
+ return false;
892
+ return keywords.includes(this.peek().value);
694
893
  }
695
- peek() {
696
- return this.tokens[this.current];
894
+ match(...types) {
895
+ for (const type of types) {
896
+ if (this.check(type)) {
897
+ this.advance();
898
+ return true;
899
+ }
900
+ }
901
+ return false;
697
902
  }
698
- previous() {
699
- return this.tokens[this.current - 1];
903
+ matchKeyword(...keywords) {
904
+ if (this.checkKeyword(...keywords)) {
905
+ this.advance();
906
+ return true;
907
+ }
908
+ return false;
700
909
  }
701
910
  consume(type, message) {
702
911
  if (this.check(type))
703
912
  return this.advance();
704
- throw new Error(`${message}. Got ${this.peek().type} at line ${this.peek().line}`);
913
+ throw new Error(`${message} at line ${this.peek().line}, got ${this.peek().type}`);
705
914
  }
706
- consumeOptional(type) {
707
- if (this.check(type)) {
915
+ consumeKeyword(...keywords) {
916
+ if (this.checkKeyword(...keywords)) {
708
917
  this.advance();
709
- return true;
710
918
  }
711
- return false;
919
+ else {
920
+ throw new Error(`Expected one of [${keywords.join(', ')}] at line ${this.peek().line}`);
921
+ }
922
+ }
923
+ consumeStatementEnd() {
924
+ if (this.match(TokenType.Semicolon) || this.match(TokenType.Newline)) {
925
+ return;
926
+ }
927
+ if (this.isAtEnd())
928
+ return;
929
+ // Allow implicit semicolon before certain tokens
930
+ if (this.check(TokenType.RightBrace) || this.checkKeyword('else')) {
931
+ return;
932
+ }
933
+ }
934
+ skipNewlines() {
935
+ const guard = createLoopGuard({
936
+ maxIterations: 100000,
937
+ context: 'newline skipping',
938
+ });
939
+ while (this.match(TokenType.Newline)) {
940
+ guard();
941
+ // Skip
942
+ }
712
943
  }
713
944
  synchronize() {
714
945
  this.advance();
715
946
  const guard = createLoopGuard({
716
947
  maxIterations: 100000,
717
- context: 'BuddyScript error synchronization',
948
+ context: 'error synchronization',
718
949
  });
719
950
  while (!this.isAtEnd()) {
720
951
  guard();
721
- if (this.previous().type === TokenType.SEMICOLON)
952
+ if (this.previous().type === TokenType.Semicolon)
953
+ return;
954
+ if (this.previous().type === TokenType.Newline)
955
+ return;
956
+ if (this.checkKeyword('if', 'for', 'while', 'let', 'const', 'func', 'function', 'class', 'return')) {
722
957
  return;
723
- switch (this.peek().type) {
724
- case TokenType.FUNCTION:
725
- case TokenType.LET:
726
- case TokenType.CONST:
727
- case TokenType.IF:
728
- case TokenType.WHILE:
729
- case TokenType.FOR:
730
- case TokenType.RETURN:
731
- case TokenType.TRY:
732
- return;
733
958
  }
734
959
  this.advance();
735
960
  }
736
961
  }
737
962
  }
963
+ /** @deprecated Use FCSParser instead */
964
+ export const Parser = FCSParser;
738
965
  export function parse(tokens) {
739
- return new Parser(tokens).parse();
966
+ return new FCSParser(tokens).parse();
740
967
  }
741
968
  //# sourceMappingURL=parser.js.map