@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 +33 -3
- package/lib/codegen/emit.js +119 -12
- package/lib/ir/transform.js +295 -39
- package/lib/ir/types.js +36 -0
- package/lib/validator/validate.js +7 -0
- package/package.json +1 -1
- package/runtime/cerver.h +23 -0
- package/runtime/static.c +12 -0
- package/runtime/str_ops.c +190 -0
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.
|
package/lib/codegen/emit.js
CHANGED
|
@@ -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);
|
package/lib/ir/transform.js
CHANGED
|
@@ -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
|
-
/*
|
|
43
|
-
|
|
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 =
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
375
|
+
/* Register so subsequent expressions can look up this variable's type */
|
|
376
|
+
ctx.types.set(name, resolvedType);
|
|
263
377
|
|
|
264
|
-
|
|
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
|
|
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
|
|
435
|
-
if (
|
|
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,
|
|
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
|
|
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
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
|
+
}
|