@velox0/cerver 0.6.4-nightly.20260605.13 → 0.6.4

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.
package/README.md CHANGED
@@ -148,16 +148,39 @@ Cerver supports a strict, synchronous subset of JavaScript suitable for C code g
148
148
  - `for` loops (`for (init; condition; update)`) and `while` loops
149
149
  - `const` / `let` variable declarations
150
150
  - String and Number literals
151
- - Template literals
152
- - Basic comparisons (`===`, `!==`, `<`, `>`)
151
+ - Template literals (`` `Hello ${name}` ``)
152
+ - Basic comparisons (`===`, `!==`, `<`, `>`, `<=`, `>=`)
153
+ - Arithmetic operators (`+`, `-`, `*`, `/`, `%`) on numeric values
154
+ - String concatenation with `+` when at least one operand is a known-string variable (auto-rewritten to a safe `snprintf` buffer)
153
155
  - Outbound API calls via `fetch(url, options)`
154
156
 
157
+ ### String Methods
158
+
159
+ The following `String.prototype` methods compile to native C:
160
+
161
+ | Method | Return | C equivalent |
162
+ |---|---|---|
163
+ | `s.toLowerCase()` | `string` | `cerver_str_tolower(s)` |
164
+ | `s.toUpperCase()` | `string` | `cerver_str_toupper(s)` |
165
+ | `s.trim()` | `string` | `cerver_str_trim(s)` |
166
+ | `s.slice(start, end)` | `string` | `cerver_str_slice(s, start, end)` |
167
+ | `s.replace(needle, replacement)` | `string` | `cerver_str_replace(s, needle, replacement)` |
168
+ | `s.concat(a, b, ...)` | `string` | `snprintf` (same path as template literals) |
169
+ | `s.includes(needle)` | `boolean` (int) | `strstr(s, needle) != NULL` |
170
+ | `s.startsWith(prefix)` | `boolean` (int) | `strncmp(s, prefix, strlen(prefix)) == 0` |
171
+ | `s.endsWith(suffix)` | `boolean` (int) | `cerver_str_endswith(s, suffix)` |
172
+ | `s.indexOf(needle)` | `number` (int) | `cerver_str_indexof(s, needle)` |
173
+ | `s.length` | `number` (int) | `(int)strlen(s)` |
174
+
175
+ String-returning methods (`toLowerCase`, `toUpperCase`, `trim`, `slice`, `replace`, `concat`) must be used as a direct variable initializer or return value — not nested inside a larger expression — because they allocate a heap buffer.
176
+
155
177
  **Not Supported (Compile-Time Errors):**
156
178
 
157
179
  - `async`/`await` and Promises
158
180
  - Classes and the `new` keyword
159
181
  - `eval()`
160
182
  - Runtime `import`/`require`
183
+ - String concatenation with `+` where both operands are string literals (use template literals instead)
161
184
 
162
185
  ## Configuration
163
186
 
@@ -176,7 +199,14 @@ export default {
176
199
 
177
200
  1. **Parser**: Uses Acorn to parse your JS route files into ASTs.
178
201
  2. **Validator**: Scans the AST to ensure no unsupported JS features are used.
179
- 3. **IR**: Transforms the AST into an Intermediate Representation.
202
+ 3. **IR**: Transforms the AST into an Intermediate Representation. Includes a symbol table that tracks variable types for correct `+` operator coercion.
180
203
  4. **Generator**: Emits optimized C code mapping directly to your JS logic.
181
204
  5. **Asset Pipeline**: Scans the `public/` folder, minifies files, and converts them to C byte arrays.
182
205
  6. **Compiler**: Invokes `gcc` or `clang` to compile the generated code and the Cerver runtime into a native binary.
206
+
207
+ ## Known Limitations / Future Work
208
+
209
+ - `replace()` only replaces the **first** occurrence (like JS). Global replace is not yet supported.
210
+ - String methods that return strings cannot be nested (e.g. `s.trim().toLowerCase()` is not supported — store the intermediate in a variable).
211
+ - The `+` type inference is forward-only; if a variable's type changes via reassignment the inferred type may be stale.
212
+ - `slice()` operates on bytes, not Unicode code points.
@@ -133,6 +133,13 @@ function emitExpression(expr) {
133
133
  return expr.prefix ? `(${op}${expr.name})` : `(${expr.name}${op})`;
134
134
  }
135
135
 
136
+ case "Arithmetic": {
137
+ /* Numeric C arithmetic — both sides are known numeric */
138
+ const left = emitExpression(expr.left);
139
+ const right = emitExpression(expr.right);
140
+ return `(${left} ${expr.operator} ${right})`;
141
+ }
142
+
136
143
  case "Concat": {
137
144
  /* Interpolated template literals need statement-level snprintf setup. */
138
145
  /* This is handled specially in emitStatement when part of a return */
@@ -147,6 +154,20 @@ function emitExpression(expr) {
147
154
  );
148
155
  }
149
156
 
157
+ case "StringOp": {
158
+ /*
159
+ * String-returning ops must be emitted at statement level (need a buffer).
160
+ * Number-returning ops can be emitted inline.
161
+ */
162
+ if (expr.returnType === "number") {
163
+ return emitStringOpInline(expr);
164
+ }
165
+ throw new Error(
166
+ `cerver: string.${expr.method}() returns a string and must be used as a ` +
167
+ `direct variable initializer or return value, not inside a larger expression.`,
168
+ );
169
+ }
170
+
150
171
  case "Call": {
151
172
  /* res.text(status, body) etc. */
152
173
  if (expr.object === "res") {
@@ -155,17 +176,6 @@ function emitExpression(expr) {
155
176
  return `${fnName}(res, ${args.join(", ")})`;
156
177
  }
157
178
 
158
- /* String method calls — map to C equivalents */
159
- if (expr.method === "toLowerCase" || expr.method === "toUpperCase") {
160
- /* These would need a helper — for now return the object as-is */
161
- return emitExpression(expr.object);
162
- }
163
- if (expr.method === "includes") {
164
- const haystack = emitExpression(expr.object);
165
- const needle = expr.args[0] ? emitExpression(expr.args[0]) : '""';
166
- return `(strstr(${haystack}, ${needle}) != NULL)`;
167
- }
168
-
169
179
  return '""';
170
180
  }
171
181
 
@@ -315,6 +325,18 @@ function assertInlineExpression(expr) {
315
325
  );
316
326
  }
317
327
 
328
+ if (
329
+ containsExpr(
330
+ expr,
331
+ (node) => node.type === "StringOp" && node.returnType === "string",
332
+ )
333
+ ) {
334
+ throw new Error(
335
+ "cerver: string-returning methods (toLowerCase, toUpperCase, trim, slice, replace) " +
336
+ "are only supported as a direct return value or variable initializer",
337
+ );
338
+ }
339
+
318
340
  if (
319
341
  containsExpr(
320
342
  expr,
@@ -332,10 +354,76 @@ function isNumberFormatExpr(expr) {
332
354
  expr &&
333
355
  (expr.type === "NumberLiteral" ||
334
356
  expr.type === "Comparison" ||
335
- expr.type === "Logical")
357
+ expr.type === "Logical" ||
358
+ expr.type === "Arithmetic" ||
359
+ (expr.type === "StringOp" && expr.returnType === "number"))
336
360
  );
337
361
  }
338
362
 
363
+ /**
364
+ * Emit a number-returning string operation inline (safe for expression context).
365
+ * Covers: includes, startsWith, endsWith, indexOf, length.
366
+ */
367
+ function emitStringOpInline(expr) {
368
+ const obj = emitExpression(expr.object);
369
+ const arg0 = expr.args[0] ? emitExpression(expr.args[0]) : '""';
370
+ const arg1 = expr.args[1] ? emitExpression(expr.args[1]) : null;
371
+
372
+ switch (expr.method) {
373
+ case "includes":
374
+ return `(strstr(${obj}, ${arg0}) != NULL)`;
375
+
376
+ case "startsWith":
377
+ return `(strncmp(${obj}, ${arg0}, strlen(${arg0})) == 0)`;
378
+
379
+ case "endsWith":
380
+ return `cerver_str_endswith(${obj}, ${arg0})`;
381
+
382
+ case "indexOf":
383
+ return `cerver_str_indexof(${obj}, ${arg0})`;
384
+
385
+ case "length":
386
+ return `((int)strlen(${obj}))`;
387
+
388
+ default:
389
+ return "0";
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Emit a heap-allocated string op result into a named buffer.
395
+ * Covers: toLowerCase, toUpperCase, trim, slice, replace.
396
+ * Returns lines of C code; varName receives the result pointer.
397
+ */
398
+ function emitStringOpBlock(expr, pad, varName) {
399
+ const obj = emitExpression(expr.object);
400
+ const arg0 = expr.args[0] ? emitExpression(expr.args[0]) : null;
401
+ const arg1 = expr.args[1] ? emitExpression(expr.args[1]) : null;
402
+
403
+ let call;
404
+ switch (expr.method) {
405
+ case "toLowerCase":
406
+ call = `cerver_str_tolower(${obj})`;
407
+ break;
408
+ case "toUpperCase":
409
+ call = `cerver_str_toupper(${obj})`;
410
+ break;
411
+ case "trim":
412
+ call = `cerver_str_trim(${obj})`;
413
+ break;
414
+ case "slice":
415
+ call = `cerver_str_slice(${obj}, ${arg0 || "0"}, ${arg1 !== null ? arg1 : "-1"})`;
416
+ break;
417
+ case "replace":
418
+ call = `cerver_str_replace(${obj}, ${arg0 || '""'}, ${arg1 || '""'})`;
419
+ break;
420
+ default:
421
+ call = `""` ;
422
+ }
423
+
424
+ return [`${pad}char *${varName} = ${call};`];
425
+ }
426
+
339
427
  /**
340
428
  * Emit a heap-backed string buffer for an interpolated template literal.
341
429
  * The response writer runs after handlers return, so response-bound strings
@@ -446,6 +534,17 @@ function emitStatement(stmt, level, ctx) {
446
534
  lines.push(`${pad}${fnName}(res, ${stmt.status}, ${tempName});`);
447
535
  lines.push(`${pad}if (${tempName}_owned) res->_body_owned = 1;`);
448
536
  lines.push(`${pad}return;`);
537
+ } else if (
538
+ stmt.value &&
539
+ stmt.value.type === "StringOp" &&
540
+ stmt.value.returnType === "string"
541
+ ) {
542
+ /* Heap-allocated string op (tolower, toupper, trim, slice, replace) */
543
+ const tempName = `_strop_res_${ctx.concatVarCounter++}`;
544
+ lines.push(...emitStringOpBlock(stmt.value, pad, tempName));
545
+ lines.push(`${pad}${fnName}(res, ${stmt.status}, ${tempName} ? ${tempName} : "");`);
546
+ lines.push(`${pad}if (${tempName}) res->_body_owned = 1;`);
547
+ lines.push(`${pad}return;`);
449
548
  } else {
450
549
  assertInlineExpression(stmt.value);
451
550
  const valueCode = emitExpression(stmt.value);
@@ -541,6 +640,14 @@ function emitStatement(stmt, level, ctx) {
541
640
  ) {
542
641
  lines.push(...emitConcatBlock(stmt.initExpr, pad, stmt.name));
543
642
  ctx.ownedStrings.add(stmt.name);
643
+ } else if (
644
+ stmt.initExpr &&
645
+ stmt.initExpr.type === "StringOp" &&
646
+ stmt.initExpr.returnType === "string"
647
+ ) {
648
+ /* Heap-allocated string op result stored in a mutable char * */
649
+ lines.push(...emitStringOpBlock(stmt.initExpr, pad, stmt.name));
650
+ ctx.ownedStrings.add(stmt.name);
544
651
  } else {
545
652
  assertInlineExpression(stmt.initExpr);
546
653
  const val = emitExpression(stmt.initExpr);
@@ -2,6 +2,121 @@
2
2
 
3
3
  const IR = require("./types");
4
4
 
5
+ /* ------------------------------------------------------------------ */
6
+ /* String method metadata */
7
+ /* ------------------------------------------------------------------ */
8
+
9
+ /**
10
+ * Supported string methods.
11
+ * returnType:
12
+ * "string" → heap-allocated result, emitted at statement level
13
+ * "number" → int, safe to emit inline
14
+ */
15
+ const STRING_METHODS = {
16
+ toLowerCase: { returnType: "string", minArgs: 0, maxArgs: 0 },
17
+ toUpperCase: { returnType: "string", minArgs: 0, maxArgs: 0 },
18
+ trim: { returnType: "string", minArgs: 0, maxArgs: 0 },
19
+ slice: { returnType: "string", minArgs: 1, maxArgs: 2 },
20
+ replace: { returnType: "string", minArgs: 2, maxArgs: 2 },
21
+ includes: { returnType: "number", minArgs: 1, maxArgs: 1 },
22
+ startsWith: { returnType: "number", minArgs: 1, maxArgs: 1 },
23
+ endsWith: { returnType: "number", minArgs: 1, maxArgs: 1 },
24
+ indexOf: { returnType: "number", minArgs: 1, maxArgs: 1 },
25
+ };
26
+
27
+ /* ------------------------------------------------------------------ */
28
+ /* Type inference helpers */
29
+ /* ------------------------------------------------------------------ */
30
+
31
+ /**
32
+ * Infer the C type of an IR expression.
33
+ * Returns "string" | "number" | "unknown".
34
+ * The symbol table (Map<name, type>) is threaded through ctx.types.
35
+ */
36
+ function inferIRType(expr, types) {
37
+ if (!expr) return "string";
38
+
39
+ switch (expr.type) {
40
+ case "StringLiteral":
41
+ case "ParamAccess":
42
+ case "QueryAccess":
43
+ case "HeaderAccess":
44
+ case "RequestField":
45
+ case "Concat":
46
+ return "string";
47
+
48
+ case "NumberLiteral":
49
+ return "number";
50
+
51
+ case "Comparison":
52
+ case "Logical":
53
+ case "Arithmetic":
54
+ return "number";
55
+
56
+ case "Unary":
57
+ return expr.operator === "!" || expr.operator === "-" ? "number" : "string";
58
+
59
+ case "Conditional": {
60
+ const c = inferIRType(expr.consequent, types);
61
+ const a = inferIRType(expr.alternate, types);
62
+ return c === "number" && a === "number" ? "number" : "string";
63
+ }
64
+
65
+ case "Identifier":
66
+ return types && types.has(expr.name) ? types.get(expr.name) : "unknown";
67
+
68
+ case "StringOp":
69
+ return expr.returnType;
70
+
71
+ case "Call":
72
+ /* fetch result is a string */
73
+ return "string";
74
+
75
+ case "Fetch":
76
+ return "string";
77
+
78
+ default:
79
+ return "unknown";
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Quick check: is an AST node provably numeric at transform time?
85
+ * Used by transformBinaryExpression to decide before the symbol table.
86
+ */
87
+ function isNumericAST(node) {
88
+ if (!node) return false;
89
+ if (node.type === "Literal" && typeof node.value === "number") return true;
90
+ if (
91
+ node.type === "BinaryExpression" &&
92
+ ["+", "-", "*", "/", "%"].includes(node.operator) &&
93
+ !(node.type === "BinaryExpression" &&
94
+ ["===", "!==", "==", "!=", "<", ">", "<=", ">="].includes(node.operator))
95
+ ) {
96
+ return isNumericAST(node.left) && isNumericAST(node.right);
97
+ }
98
+ if (node.type === "UnaryExpression" && node.operator === "-") {
99
+ return isNumericAST(node.argument);
100
+ }
101
+ return false;
102
+ }
103
+
104
+ /**
105
+ * Quick check: is an AST node provably string at transform time?
106
+ */
107
+ function isStringAST(node) {
108
+ if (!node) return false;
109
+ if (node.type === "Literal" && typeof node.value === "string") return true;
110
+ if (node.type === "TemplateLiteral") return true;
111
+ /* Member expressions like req.params.x, req.query.x are always strings */
112
+ if (node.type === "MemberExpression") return true;
113
+ return false;
114
+ }
115
+
116
+ /* ------------------------------------------------------------------ */
117
+ /* Top-level file transform */
118
+ /* ------------------------------------------------------------------ */
119
+
5
120
  /**
6
121
  * Transform a validated AST into an IR route descriptor.
7
122
  *
@@ -39,8 +154,12 @@ function transformFile(ast, urlPath) {
39
154
  ? funcDecl.params[1].name
40
155
  : "res";
41
156
 
42
- /* Transform the function body */
43
- const ctx = { reqName, resName };
157
+ /*
158
+ * ctx carries:
159
+ * reqName, resName — parameter names in scope
160
+ * types — symbol table: Map<varName, "string"|"number">
161
+ */
162
+ const ctx = { reqName, resName, types: new Map() };
44
163
  const { variables, body } = transformBlock(funcDecl.body, ctx, {
45
164
  hoistVariables: true,
46
165
  });
@@ -53,6 +172,10 @@ function transformFile(ast, urlPath) {
53
172
  return routes;
54
173
  }
55
174
 
175
+ /* ------------------------------------------------------------------ */
176
+ /* Block / statement transform */
177
+ /* ------------------------------------------------------------------ */
178
+
56
179
  /**
57
180
  * Transform a block statement into IR variables and statements.
58
181
  */
@@ -105,6 +228,10 @@ function transformStatement(node, ctx, options) {
105
228
  }
106
229
  }
107
230
 
231
+ /* ------------------------------------------------------------------ */
232
+ /* Statement-level transforms */
233
+ /* ------------------------------------------------------------------ */
234
+
108
235
  /**
109
236
  * Transform a return statement.
110
237
  *
@@ -230,6 +357,7 @@ function transformFor(node, ctx) {
230
357
 
231
358
  /**
232
359
  * Transform a variable declaration.
360
+ * Registers the inferred type in ctx.types for later use.
233
361
  */
234
362
  function transformVariableDecl(node, ctx) {
235
363
  /* For simplicity, handle the first declarator */
@@ -241,37 +369,19 @@ function transformVariableDecl(node, ctx) {
241
369
  ? transformExpression(decl.init, ctx)
242
370
  : IR.IRStringLiteral("");
243
371
 
244
- const valueType = inferValueType(initExpr);
245
-
246
- return IR.IRVariable(name, valueType, initExpr);
247
- }
248
-
249
- function inferValueType(expr) {
250
- if (!expr) return "string";
251
-
252
- if (
253
- expr.type === "NumberLiteral" ||
254
- expr.type === "Comparison" ||
255
- expr.type === "Logical"
256
- ) {
257
- return "number";
258
- }
372
+ const valueType = inferIRType(initExpr, ctx.types);
373
+ const resolvedType = valueType === "unknown" ? "string" : valueType;
259
374
 
260
- if (expr.type === "Unary") {
261
- return expr.operator === "!" || expr.operator === "-" ? "number" : "string";
262
- }
375
+ /* Register so subsequent expressions can look up this variable's type */
376
+ ctx.types.set(name, resolvedType);
263
377
 
264
- if (expr.type === "Conditional") {
265
- const consequentType = inferValueType(expr.consequent);
266
- const alternateType = inferValueType(expr.alternate);
267
- return consequentType === "number" && alternateType === "number"
268
- ? "number"
269
- : "string";
270
- }
271
-
272
- return "string";
378
+ return IR.IRVariable(name, resolvedType, initExpr);
273
379
  }
274
380
 
381
+ /* ------------------------------------------------------------------ */
382
+ /* Expression transform */
383
+ /* ------------------------------------------------------------------ */
384
+
275
385
  /**
276
386
  * Transform an expression into an IR expression node.
277
387
  */
@@ -287,11 +397,7 @@ function transformExpression(node, ctx) {
287
397
  return IR.IRIdentifier(node.name);
288
398
 
289
399
  case "BinaryExpression":
290
- return IR.IRComparison(
291
- node.operator,
292
- transformExpression(node.left, ctx),
293
- transformExpression(node.right, ctx),
294
- );
400
+ return transformBinaryExpression(node, ctx);
295
401
 
296
402
  case "LogicalExpression":
297
403
  return IR.IRLogical(
@@ -330,11 +436,116 @@ function transformExpression(node, ctx) {
330
436
  }
331
437
  }
332
438
 
439
+ /**
440
+ * Helper to flatten nested Concat nodes and merge adjacent StringLiterals.
441
+ */
442
+ function createConcat(parts) {
443
+ const flat = [];
444
+ for (const p of parts) {
445
+ if (p.type === "Concat") {
446
+ flat.push(...p.parts);
447
+ } else {
448
+ flat.push(p);
449
+ }
450
+ }
451
+
452
+ const merged = [];
453
+ for (const p of flat) {
454
+ if (p.type === "StringLiteral") {
455
+ if (merged.length > 0 && merged[merged.length - 1].type === "StringLiteral") {
456
+ merged[merged.length - 1].value += p.value;
457
+ } else {
458
+ merged.push({ ...p });
459
+ }
460
+ } else {
461
+ merged.push(p);
462
+ }
463
+ }
464
+
465
+ if (merged.length === 1) return merged[0];
466
+ return IR.IRConcat(merged);
467
+ }
468
+
469
+ /* ------------------------------------------------------------------ */
470
+ /* Binary expression: +, -, *, /, %, comparisons */
471
+ /* ------------------------------------------------------------------ */
472
+
473
+ /**
474
+ * Transform a binary expression.
475
+ */
476
+ function transformBinaryExpression(node, ctx) {
477
+ const arithmeticOps = new Set(["+", "-", "*", "/", "%"]);
478
+ const comparisonOps = new Set(["===", "!==", "==", "!=", "<", ">", "<=", ">="]);
479
+
480
+ if (comparisonOps.has(node.operator)) {
481
+ return IR.IRComparison(
482
+ node.operator,
483
+ transformExpression(node.left, ctx),
484
+ transformExpression(node.right, ctx),
485
+ );
486
+ }
487
+
488
+ if (!arithmeticOps.has(node.operator)) {
489
+ return IR.IRStringLiteral("");
490
+ }
491
+
492
+ if (node.operator !== "+") {
493
+ return IR.IRArithmetic(
494
+ node.operator,
495
+ transformExpression(node.left, ctx),
496
+ transformExpression(node.right, ctx),
497
+ );
498
+ }
499
+
500
+ const leftIsNumericAST = isNumericAST(node.left);
501
+ const rightIsNumericAST = isNumericAST(node.right);
502
+ const leftIsStringAST = isStringAST(node.left);
503
+ const rightIsStringAST = isStringAST(node.right);
504
+
505
+ const leftIR = transformExpression(node.left, ctx);
506
+ const rightIR = transformExpression(node.right, ctx);
507
+
508
+ const leftType = inferIRType(leftIR, ctx.types);
509
+ const rightType = inferIRType(rightIR, ctx.types);
510
+
511
+ if (
512
+ (leftIsNumericAST || leftType === "number") &&
513
+ (rightIsNumericAST || rightType === "number")
514
+ ) {
515
+ return IR.IRArithmetic("+", leftIR, rightIR);
516
+ }
517
+
518
+ if (
519
+ leftIsStringAST || rightIsStringAST ||
520
+ leftType === "string" || rightType === "string"
521
+ ) {
522
+ return createConcat([leftIR, rightIR]);
523
+ }
524
+
525
+ throw new Error(
526
+ `cerver: '+' with operands of unknown type — cannot determine whether this is ` +
527
+ `numeric addition or string concatenation. ` +
528
+ `Declare variables with explicit initial values so the type can be inferred, ` +
529
+ `or use a template literal (\`...\${expression}...\`) for string concatenation.`,
530
+ );
531
+ }
532
+
533
+ /* ------------------------------------------------------------------ */
534
+ /* Assignment / Update */
535
+ /* ------------------------------------------------------------------ */
536
+
333
537
  function transformAssignmentExpr(node, ctx) {
334
538
  if (!node.left || node.left.type !== "Identifier") {
335
539
  return IR.IRStringLiteral("");
336
540
  }
337
541
  const value = transformExpression(node.right, ctx);
542
+
543
+ /* Keep the symbol table consistent for += on string variables */
544
+ if (node.operator === "=" && ctx.types) {
545
+ const t = inferIRType(value, ctx.types);
546
+ if (t !== "unknown") ctx.types.set(node.left.name, t);
547
+ }
548
+
338
549
  return IR.IRAssignment(node.left.name, node.operator, value);
339
550
  }
340
551
 
@@ -345,6 +556,10 @@ function transformUpdateExpr(node) {
345
556
  return IR.IRUpdate(node.argument.name, node.operator, node.prefix);
346
557
  }
347
558
 
559
+ /* ------------------------------------------------------------------ */
560
+ /* Member expressions */
561
+ /* ------------------------------------------------------------------ */
562
+
348
563
  /**
349
564
  * Transform member expressions.
350
565
  *
@@ -352,6 +567,7 @@ function transformUpdateExpr(node) {
352
567
  * req.params.key → IRParamAccess("key")
353
568
  * req.query.x → IRQueryAccess("x")
354
569
  * req.headers.x → IRHeaderAccess("x")
570
+ * str.length → IRStringOp("length", str, [], "number")
355
571
  */
356
572
  function transformMemberExpr(node, ctx) {
357
573
  /* req.params.key or req.query.key */
@@ -375,9 +591,22 @@ function transformMemberExpr(node, ctx) {
375
591
  if (prop === "path") return IR.IRRequestField("path");
376
592
  }
377
593
 
594
+ /* str.length — property access, not a call */
595
+ if (!node.computed) {
596
+ const prop = node.property.name;
597
+ if (prop === "length") {
598
+ const obj = transformExpression(node.object, ctx);
599
+ return IR.IRStringOp("length", obj, [], "number");
600
+ }
601
+ }
602
+
378
603
  return IR.IRStringLiteral("");
379
604
  }
380
605
 
606
+ /* ------------------------------------------------------------------ */
607
+ /* Call expressions */
608
+ /* ------------------------------------------------------------------ */
609
+
381
610
  /**
382
611
  * Transform call expressions.
383
612
  */
@@ -431,17 +660,44 @@ function transformCallExpr(node, ctx) {
431
660
  return IR.IRFetch(urlExpr, methodExpr, bodyExpr, headersArr);
432
661
  }
433
662
 
434
- /* String methods like str.toLowerCase(), includes() etc. — return as-is */
435
- if (node.callee.type === "MemberExpression") {
663
+ /* String method calls: str.toLowerCase(), str.includes(n), etc. */
664
+ if (
665
+ node.callee.type === "MemberExpression" &&
666
+ !node.callee.computed
667
+ ) {
668
+ const methodName = node.callee.property.name;
669
+
670
+ /* str.concat(a, b, ...) → IRConcat([obj, a, b, ...])
671
+ * Variadic, same snprintf path as template literals and "+". */
672
+ if (methodName === "concat") {
673
+ const obj = transformExpression(node.callee.object, ctx);
674
+ const args = node.arguments.map((a) => transformExpression(a, ctx));
675
+ const parts = [obj, ...args];
676
+ if (parts.length === 1) return parts[0];
677
+ return createConcat(parts);
678
+ }
679
+
680
+ const meta = STRING_METHODS[methodName];
681
+
682
+ if (meta) {
683
+ const obj = transformExpression(node.callee.object, ctx);
684
+ const args = node.arguments.map((a) => transformExpression(a, ctx));
685
+ return IR.IRStringOp(methodName, obj, args, meta.returnType);
686
+ }
687
+
688
+ /* Generic method call — pass through as IRCall (for res.* etc.) */
436
689
  const obj = transformExpression(node.callee.object, ctx);
437
- const method = node.callee.property.name;
438
690
  const args = node.arguments.map((a) => transformExpression(a, ctx));
439
- return IR.IRCall(obj, method, args);
691
+ return IR.IRCall(obj, methodName, args);
440
692
  }
441
693
 
442
694
  return IR.IRStringLiteral("");
443
695
  }
444
696
 
697
+ /* ------------------------------------------------------------------ */
698
+ /* Template literals */
699
+ /* ------------------------------------------------------------------ */
700
+
445
701
  /**
446
702
  * Transform template literals into concatenation.
447
703
  */
@@ -459,7 +715,7 @@ function transformTemplateLiteral(node, ctx) {
459
715
  }
460
716
 
461
717
  if (parts.length === 1) return parts[0];
462
- return IR.IRConcat(parts);
718
+ return createConcat(parts);
463
719
  }
464
720
 
465
721
  module.exports = { transformFile };
package/lib/ir/types.js CHANGED
@@ -267,6 +267,39 @@ function IRFetch(url, method, body, headers) {
267
267
  };
268
268
  }
269
269
 
270
+ /**
271
+ * A numeric arithmetic expression (+, -, *, /, %).
272
+ * Distinct from IRComparison (which uses strcmp for strings).
273
+ * Both operands must be numeric.
274
+ */
275
+ function IRArithmetic(operator, left, right) {
276
+ return {
277
+ type: "Arithmetic",
278
+ operator /* "+" | "-" | "*" | "/" | "%" */,
279
+ left /* IRExpression */,
280
+ right /* IRExpression */,
281
+ };
282
+ }
283
+
284
+ /**
285
+ * A compiled string method call.
286
+ *
287
+ * @param {string} method - The JS method name ("toLowerCase", "includes", etc.)
288
+ * @param {object} object - IR expression for the string being operated on
289
+ * @param {object[]} args - IR expressions for method arguments
290
+ * @param {string} returnType - "string" | "number" — drives emit strategy
291
+ */
292
+ function IRStringOp(method, object, args, returnType) {
293
+ return {
294
+ type: "StringOp",
295
+ method,
296
+ object,
297
+ args,
298
+ returnType /* "string" (heap-alloc) | "number" (inline) */,
299
+ };
300
+ }
301
+
302
+
270
303
  module.exports = {
271
304
  IRRoute,
272
305
  IRHandler,
@@ -291,4 +324,7 @@ module.exports = {
291
324
  IRConcat,
292
325
  IRCall,
293
326
  IRFetch,
327
+ IRArithmetic,
328
+ IRStringOp,
294
329
  };
330
+
@@ -227,6 +227,13 @@ function validate(ast, filePath, source) {
227
227
  );
228
228
  }
229
229
  }
230
+
231
+ /*
232
+ * "+" operator: fully handled by the transform pass's symbol table.
233
+ * String literal + string literal, identifier + string, numeric + numeric —
234
+ * all are correctly lowered to IRArithmetic, IRConcat, or a compile-time
235
+ * error with good diagnostics. Nothing to catch here.
236
+ */
230
237
  });
231
238
 
232
239
  if (errors.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velox0/cerver",
3
- "version": "0.6.4-nightly.20260605.13",
3
+ "version": "0.6.4",
4
4
  "description": "Compile restricted JavaScript server logic into optimized native C binaries (cross-platform: Linux, macOS, Windows)",
5
5
  "main": "bin/cerver.js",
6
6
  "bin": {
package/runtime/cerver.h CHANGED
@@ -282,6 +282,29 @@ void cerver_trie_free(void* trie);
282
282
  */
283
283
  char* cerver_fetch(const char* url, const char* method, const char* body, const char** headers);
284
284
 
285
+ /* ------------------------------------------------------------------ */
286
+ /* String operations (generated route code) */
287
+ /* ------------------------------------------------------------------ */
288
+
289
+ /* Case conversion — return malloc'd strings (caller must free) */
290
+ char* cerver_str_tolower(const char* s);
291
+ char* cerver_str_toupper(const char* s);
292
+
293
+ /* Whitespace removal — returns malloc'd string */
294
+ char* cerver_str_trim(const char* s);
295
+
296
+ /* Substring — returns malloc'd string; end=-1 means "to end of string" */
297
+ char* cerver_str_slice(const char* s, int start, int end);
298
+
299
+ /* First-occurrence replace — returns malloc'd string */
300
+ char* cerver_str_replace(const char* s, const char* needle, const char* replacement);
301
+
302
+ /* Predicate helpers — return int, safe for inline expressions */
303
+ int cerver_str_endswith(const char* s, const char* suffix);
304
+ int cerver_str_indexof(const char* s, const char* needle);
305
+
306
+
307
+
285
308
  /* ------------------------------------------------------------------ */
286
309
  /* MIME (internal) */
287
310
  /* ------------------------------------------------------------------ */
package/runtime/static.c CHANGED
@@ -226,12 +226,24 @@ static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_
226
226
  char full_path[CERVER_MAX_PATH * 2];
227
227
  snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, path);
228
228
 
229
+ #if CERVER_PLATFORM_WINDOWS
230
+ /* Normalize forward slashes to backslashes for native Windows APIs */
231
+ for (char* p = full_path; *p; p++) {
232
+ if (*p == '/') *p = '\\';
233
+ }
234
+ #endif
235
+
229
236
  /* Check if it's a directory — try fallback path */
230
237
  struct stat st;
231
238
  if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
232
239
  char fallback_path[CERVER_MAX_PATH];
233
240
  get_fallback_path(path, fallback_path, sizeof(fallback_path));
234
241
  snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, fallback_path);
242
+ #if CERVER_PLATFORM_WINDOWS
243
+ for (char* p = full_path; *p; p++) {
244
+ if (*p == '/') *p = '\\';
245
+ }
246
+ #endif
235
247
  if (stat(full_path, &st) != 0) return -1;
236
248
  }
237
249
 
@@ -0,0 +1,190 @@
1
+ /*
2
+ * str_ops.c — String operation helpers for cerver generated code.
3
+ *
4
+ * These functions provide the C implementations for JavaScript string
5
+ * methods compiled by cerver. All functions that return strings return
6
+ * heap-allocated buffers (malloc). Callers are responsible for freeing
7
+ * the returned pointer — the generated code tracks ownership via the
8
+ * _body_owned flag on cerver_response_t.
9
+ *
10
+ * Functions that return int are safe to call inline.
11
+ */
12
+
13
+ #include <ctype.h>
14
+ #include <stddef.h>
15
+ #include <stdlib.h>
16
+ #include <string.h>
17
+
18
+ /* ------------------------------------------------------------------ */
19
+ /* Case conversion */
20
+ /* ------------------------------------------------------------------ */
21
+
22
+ /**
23
+ * Return a malloc'd copy of `s` with all ASCII characters lowercased.
24
+ * Returns NULL on allocation failure.
25
+ */
26
+ char *cerver_str_tolower(const char *s) {
27
+ if (!s) return NULL;
28
+ size_t len = strlen(s);
29
+ char *out = (char *)malloc(len + 1);
30
+ if (!out) return NULL;
31
+ for (size_t i = 0; i <= len; i++) {
32
+ out[i] = (char)tolower((unsigned char)s[i]);
33
+ }
34
+ return out;
35
+ }
36
+
37
+ /**
38
+ * Return a malloc'd copy of `s` with all ASCII characters uppercased.
39
+ * Returns NULL on allocation failure.
40
+ */
41
+ char *cerver_str_toupper(const char *s) {
42
+ if (!s) return NULL;
43
+ size_t len = strlen(s);
44
+ char *out = (char *)malloc(len + 1);
45
+ if (!out) return NULL;
46
+ for (size_t i = 0; i <= len; i++) {
47
+ out[i] = (char)toupper((unsigned char)s[i]);
48
+ }
49
+ return out;
50
+ }
51
+
52
+ /* ------------------------------------------------------------------ */
53
+ /* Trim */
54
+ /* ------------------------------------------------------------------ */
55
+
56
+ /**
57
+ * Return a malloc'd copy of `s` with leading and trailing ASCII
58
+ * whitespace removed. Returns NULL on allocation failure.
59
+ */
60
+ char *cerver_str_trim(const char *s) {
61
+ if (!s) return NULL;
62
+
63
+ /* Skip leading whitespace */
64
+ while (*s && isspace((unsigned char)*s)) s++;
65
+
66
+ const char *end = s + strlen(s);
67
+ /* Skip trailing whitespace */
68
+ while (end > s && isspace((unsigned char)*(end - 1))) end--;
69
+
70
+ size_t len = (size_t)(end - s);
71
+ char *out = (char *)malloc(len + 1);
72
+ if (!out) return NULL;
73
+ memcpy(out, s, len);
74
+ out[len] = '\0';
75
+ return out;
76
+ }
77
+
78
+ /* ------------------------------------------------------------------ */
79
+ /* Slice */
80
+ /* ------------------------------------------------------------------ */
81
+
82
+ /**
83
+ * Return a malloc'd substring of `s` from byte index `start` (inclusive)
84
+ * to `end` (exclusive), mirroring JS Array/String.prototype.slice().
85
+ *
86
+ * Negative indices count from the end of the string.
87
+ * Pass end = -1 to mean "to the end of the string".
88
+ *
89
+ * Returns NULL on allocation failure.
90
+ */
91
+ char *cerver_str_slice(const char *s, int start, int end) {
92
+ if (!s) return NULL;
93
+ int len = (int)strlen(s);
94
+
95
+ /* Resolve negative indices */
96
+ if (start < 0) start = len + start;
97
+ if (end < 0) end = (end == -1) ? len : len + end;
98
+
99
+ /* Clamp */
100
+ if (start < 0) start = 0;
101
+ if (start > len) start = len;
102
+ if (end < 0) end = 0;
103
+ if (end > len) end = len;
104
+ if (start > end) { int t = start; start = end; end = t; }
105
+
106
+ int out_len = end - start;
107
+ char *out = (char *)malloc((size_t)out_len + 1);
108
+ if (!out) return NULL;
109
+ memcpy(out, s + start, (size_t)out_len);
110
+ out[out_len] = '\0';
111
+ return out;
112
+ }
113
+
114
+ /* ------------------------------------------------------------------ */
115
+ /* Replace (first occurrence) */
116
+ /* ------------------------------------------------------------------ */
117
+
118
+ /**
119
+ * Return a malloc'd copy of `s` with the first occurrence of `needle`
120
+ * replaced by `replacement`. If `needle` is not found, returns a copy
121
+ * of `s`. Returns NULL on allocation failure.
122
+ */
123
+ char *cerver_str_replace(const char *s, const char *needle,
124
+ const char *replacement) {
125
+ if (!s) return NULL;
126
+ if (!needle || *needle == '\0') {
127
+ /* Empty needle — return a copy */
128
+ size_t len = strlen(s);
129
+ char *out = (char *)malloc(len + 1);
130
+ if (!out) return NULL;
131
+ memcpy(out, s, len + 1);
132
+ return out;
133
+ }
134
+
135
+ const char *pos = strstr(s, needle);
136
+ if (!pos) {
137
+ /* Needle not found — return a copy of s */
138
+ size_t len = strlen(s);
139
+ char *out = (char *)malloc(len + 1);
140
+ if (!out) return NULL;
141
+ memcpy(out, s, len + 1);
142
+ return out;
143
+ }
144
+
145
+ size_t needle_len = strlen(needle);
146
+ size_t replacement_len = replacement ? strlen(replacement) : 0;
147
+ size_t before_len = (size_t)(pos - s);
148
+ size_t after_len = strlen(pos + needle_len);
149
+ size_t total = before_len + replacement_len + after_len + 1;
150
+
151
+ char *out = (char *)malloc(total);
152
+ if (!out) return NULL;
153
+
154
+ char *p = out;
155
+ memcpy(p, s, before_len);
156
+ p += before_len;
157
+ if (replacement_len) {
158
+ memcpy(p, replacement, replacement_len);
159
+ p += replacement_len;
160
+ }
161
+ memcpy(p, pos + needle_len, after_len + 1); /* +1 for NUL */
162
+ return out;
163
+ }
164
+
165
+ /* ------------------------------------------------------------------ */
166
+ /* Predicate helpers (return int, safe for inline expression) */
167
+ /* ------------------------------------------------------------------ */
168
+
169
+ /**
170
+ * Return 1 if `s` ends with `suffix`, 0 otherwise.
171
+ * Mirrors JS String.prototype.endsWith().
172
+ */
173
+ int cerver_str_endswith(const char *s, const char *suffix) {
174
+ if (!s || !suffix) return 0;
175
+ size_t slen = strlen(s);
176
+ size_t suffixlen = strlen(suffix);
177
+ if (suffixlen > slen) return 0;
178
+ return memcmp(s + slen - suffixlen, suffix, suffixlen) == 0;
179
+ }
180
+
181
+ /**
182
+ * Return the byte index of the first occurrence of `needle` in `s`,
183
+ * or -1 if not found. Mirrors JS String.prototype.indexOf().
184
+ */
185
+ int cerver_str_indexof(const char *s, const char *needle) {
186
+ if (!s || !needle) return -1;
187
+ const char *pos = strstr(s, needle);
188
+ if (!pos) return -1;
189
+ return (int)(pos - s);
190
+ }