1use std::collections::BTreeMap;
11
12use crate::stdlib::StdlibRegistry;
13
14const MODULE_ORDER: &[&str] = &[
20 "core",
21 "math",
22 "string",
23 "list",
24 "record",
25 "time",
26 "convert",
27 "json",
28 "timer",
29 "http",
30 "storage",
31 "location",
32 "notifications",
33];
34
35fn stdlib_descriptions() -> BTreeMap<(&'static str, &'static str), &'static str> {
38 let mut d = BTreeMap::new();
39
40 d.insert(("core", "log"), "Debug logging (no-op in production, writes to console in dev)");
42 d.insert(("core", "assert"), "Panics (WASM trap) if condition is false");
43 d.insert(("core", "type_of"), "Returns type name: \"number\", \"string\", \"bool\", \"nil\", \"list\", \"record\"");
44 d.insert(("core", "capability"), "Returns whether a declared optional capability is available at runtime");
45
46 d.insert(("math", "abs"), "Absolute value");
48 d.insert(("math", "min"), "Smaller of two values");
49 d.insert(("math", "max"), "Larger of two values");
50 d.insert(("math", "floor"), "Round down to nearest integer");
51 d.insert(("math", "ceil"), "Round up to nearest integer");
52 d.insert(("math", "round"), "Round to nearest integer (0.5 rounds up)");
53 d.insert(("math", "round_to"), "Round to N decimal places");
54 d.insert(("math", "pow"), "Exponentiation");
55 d.insert(("math", "clamp"), "Clamp value to [min, max] range");
56 d.insert(("math", "sqrt"), "Square root");
57
58 d.insert(("string", "length"), "Number of characters");
60 d.insert(("string", "concat"), "Concatenate two strings");
61 d.insert(("string", "contains"), "True if needle found in haystack");
62 d.insert(("string", "slice"), "Substring from start (inclusive) to end (exclusive)");
63 d.insert(("string", "trim"), "Remove leading/trailing whitespace");
64 d.insert(("string", "split"), "Split string by delimiter");
65 d.insert(("string", "to_upper"), "Convert to uppercase");
66 d.insert(("string", "to_lower"), "Convert to lowercase");
67 d.insert(("string", "starts_with"), "True if s starts with prefix");
68 d.insert(("string", "ends_with"), "True if s ends with suffix");
69 d.insert(("string", "replace"), "Replace first occurrence of old with new");
70 d.insert(("string", "replace_all"), "Replace all occurrences of old with new");
71 d.insert(("string", "pad_start"), "Pad string on the left to reach target length");
72 d.insert(("string", "pad_end"), "Pad string on the right to reach target length");
73 d.insert(("string", "repeat"), "Repeat string count times");
74 d.insert(("string", "join"), "Join list of strings with separator");
75 d.insert(("string", "format"), "Template string with {key} placeholders replaced by record values");
76 d.insert(("string", "from"), "Convert any value to its string representation");
77 d.insert(("string", "is_empty"), "True if string has zero length");
78 d.insert(("string", "index_of"), "Index of first occurrence of sub, or -1 if not found");
79
80 d.insert(("list", "empty"), "Create empty typed list");
82 d.insert(("list", "of"), "Create list from arguments (compiler-special-cased variadic)");
83 d.insert(("list", "repeat"), "Create list of count copies of item");
84 d.insert(("list", "range"), "Generate list of integers from start (inclusive) to end (exclusive)");
85 d.insert(("list", "length"), "Number of elements");
86 d.insert(("list", "get"), "Get item at index, returns nil if out of bounds");
87 d.insert(("list", "first"), "First element, or nil if empty");
88 d.insert(("list", "last"), "Last element, or nil if empty");
89 d.insert(("list", "index_of"), "Index of first occurrence of item, or -1 if not found");
90 d.insert(("list", "append"), "Return new list with item added at end");
91 d.insert(("list", "prepend"), "Return new list with item added at start");
92 d.insert(("list", "insert"), "Return new list with item inserted at index");
93 d.insert(("list", "remove"), "Return new list with item at index removed");
94 d.insert(("list", "update"), "Return new list with item at index replaced");
95 d.insert(("list", "set"), "Return new list with item at index replaced (alias for update)");
96 d.insert(("list", "slice"), "Sublist from start (inclusive) to end (exclusive)");
97 d.insert(("list", "concat"), "Concatenate two lists");
98 d.insert(("list", "reverse"), "Return reversed list");
99 d.insert(("list", "flatten"), "Flatten one level of nesting");
100 d.insert(("list", "unique"), "Remove duplicate elements (preserves first occurrence)");
101 d.insert(("list", "map"), "Transform each element");
102 d.insert(("list", "filter"), "Keep elements where fn returns true");
103 d.insert(("list", "reduce"), "Left fold with initial value");
104 d.insert(("list", "find"), "First element where fn returns true, or nil");
105 d.insert(("list", "find_index"), "Index of first element where fn returns true, or -1");
106 d.insert(("list", "every"), "True if fn returns true for every element");
107 d.insert(("list", "any"), "True if fn returns true for any element");
108 d.insert(("list", "some"), "Alias for list.any (backward compatibility)");
109 d.insert(("list", "sort"), "Sort by comparator function");
110 d.insert(("list", "contains"), "True if item is in list");
111 d.insert(("list", "count"), "Count elements where fn returns true");
112 d.insert(("list", "zip"), "Combine two lists element-wise into list of pairs");
113 d.insert(("list", "take"), "Return first count elements");
114 d.insert(("list", "drop"), "Return all elements after the first count");
115
116 d.insert(("record", "get"), "Get field value by name");
118 d.insert(("record", "set"), "Return new record with field updated");
119 d.insert(("record", "has"), "True if record has the named field");
120 d.insert(("record", "keys"), "List of field names");
121 d.insert(("record", "values"), "List of field values");
122
123 d.insert(("time", "now"), "Current timestamp in milliseconds (host-provided)");
125 d.insert(("time", "format"), "Format timestamp with pattern (YYYY-MM-DD, HH:mm, etc.)");
126 d.insert(("time", "diff"), "Difference in milliseconds (a - b)");
127 d.insert(("time", "day_of_week"), "0=Sunday through 6=Saturday");
128 d.insert(("time", "start_of_day"), "Timestamp of midnight (00:00) for the given day");
129
130 d.insert(("convert", "to_string"), "Convert any value to string representation");
132 d.insert(("convert", "to_number"), "Convert to number (parses strings, bool->0/1), returns Result");
133 d.insert(("convert", "parse_int"), "Parse string to integer, returns Result");
134 d.insert(("convert", "parse_float"), "Parse string to float, returns Result");
135 d.insert(("convert", "to_bool"), "Truthy conversion (0/nil/\"\" -> false, else true)");
136
137 d.insert(("json", "parse"), "Parse JSON string to PEPL value, returns Result");
139 d.insert(("json", "stringify"), "Serialize PEPL value to JSON string");
140
141 d.insert(("timer", "start"), "Start recurring timer dispatching action at interval, returns ID");
143 d.insert(("timer", "start_once"), "Schedule one-shot action dispatch after delay, returns ID");
144 d.insert(("timer", "stop"), "Stop a running timer by ID");
145 d.insert(("timer", "stop_all"), "Stop all active timers for this space");
146
147 d.insert(("http", "get"), "HTTP GET request, returns Result<string, string>");
149 d.insert(("http", "post"), "HTTP POST request, returns Result<string, string>");
150 d.insert(("http", "put"), "HTTP PUT request, returns Result<string, string>");
151 d.insert(("http", "patch"), "HTTP PATCH request, returns Result<string, string>");
152 d.insert(("http", "delete"), "HTTP DELETE request, returns Result<string, string>");
153
154 d.insert(("storage", "get"), "Get stored value by key, returns string or nil");
156 d.insert(("storage", "set"), "Store a key-value pair");
157 d.insert(("storage", "delete"), "Delete a stored key");
158 d.insert(("storage", "keys"), "List all stored keys");
159
160 d.insert(("location", "current"), "Get current location as { lat: number, lon: number }");
162
163 d.insert(("notifications", "send"), "Send a notification with title and body");
165
166 d
167}
168
169fn constant_descriptions() -> BTreeMap<(&'static str, &'static str), &'static str> {
171 let mut d = BTreeMap::new();
172 d.insert(("math", "PI"), "Pi (3.14159265358979...)");
173 d.insert(("math", "E"), "Euler's number (2.71828182845904...)");
174 d
175}
176
177pub fn generate_reference() -> String {
186 let reg = StdlibRegistry::new();
187 let mut out = String::with_capacity(4096);
188
189 out.push_str(REFERENCE_PREAMBLE);
191
192 out.push_str("STDLIB (always available, no imports):\n");
194 for &module_name in MODULE_ORDER {
195 if let Some(funcs) = reg.modules().get(module_name) {
196 let mut names: Vec<&String> = funcs.keys().collect();
197 names.sort();
198
199 let const_names: Vec<&String> = reg
201 .all_constants()
202 .get(module_name)
203 .map(|c| c.keys().collect())
204 .unwrap_or_default();
205
206 let all_names: Vec<String> = names
207 .iter()
208 .map(|n| n.to_string())
209 .chain(const_names.iter().map(|n| n.to_string()))
210 .collect();
211
212 out.push_str(&format!(" {}: {}\n", module_name, all_names.join(", ")));
213 }
214 }
215 out.push('\n');
216
217 out.push_str(REFERENCE_POSTAMBLE);
219
220 out
221}
222
223const REFERENCE_PREAMBLE: &str = r#"PEPL: deterministic, sandboxed language. Compiles to WASM. One space per file.
225Comments: // only (no block comments)
226
227STRUCTURE (block order enforced):
228 space Name {
229 types { type X = | A | B(field: type) }
230 state { field: type = default }
231 capabilities { required: [http, storage] optional: [location] }
232 credentials { api_key: string }
233 derived { full_name: string = "${first} ${last}" }
234 invariants { name { bool_expression } }
235 actions { action name(p: type) { set field = value } }
236 views { view main() -> Surface { Column { Text { value: "hi" } } } }
237 update(dt: number) { ... } // optional — game/animation loop
238 handleEvent(event: InputEvent) { ... } // optional — game/interactive input
239 }
240 // Tests go OUTSIDE the space:
241 tests { test "name" { assert expression } }
242
243TYPES: number, string, bool, nil, color
244 number covers integers, floats, AND timestamps/durations (Unix ms)
245 No timestamp or duration types — use number
246COMPOSITES: list<T>, { field: type }
247 No record<{}> — use { field: type } inline
248SUM TYPES: type Name = | Variant1(field: type) | Variant2
249RESULT: type Result<T, E> = | Ok(value: T) | Err(error: E)
250 No user-defined generics — only built-in list<T>, Result<T,E>
251
252CONTROL FLOW:
253 if cond { ... } else { ... }
254 for item in list { ... }
255 for item, index in list { ... } // optional index binding
256 match expr { Pattern(bind) -> result, _ -> default }
257 let name: type = expression // immutable binding
258 set field = expression // state mutation (actions only)
259 set record.field = expression // sugar for { ...record, field: expr }
260 return // early exit from action (no value)
261
262OPERATORS:
263 Arithmetic: + - * / %
264 Comparison: == != < > <= >=
265 Logical: not and or
266 Result unwrap: expr? // postfix — traps on Err
267 Nil-coalescing: expr ?? fallback
268 Record spread: { ...base, field: val }
269
270NIL NARROWING:
271 if x != nil { ... } // x narrows from T|nil to T in block
272 let item = list.get(items, i) ?? fallback // also valid
273
274STRING INTERPOLATION: "Hello ${name}, you have ${count} items"
275
276LAMBDAS (block-body only):
277 fn(x) { x * 2 } // no expression-body shorthand
278 Return value = last expression in block body. No `return` in lambdas.
279 match can be used as expression or standalone statement.
280
281"#;
282
283const REFERENCE_POSTAMBLE: &str = r#" No operator duplicates (no core.eq, math.add, etc.)
285 string.replace replaces FIRST occurrence only — use string.replace_all for all
286
287CAPABILITIES (require declaration + host support):
288 http: get, post, put, patch, delete — all return Result<HttpResponse, HttpError>
289 options: { headers: [...], timeout: number, content_type: string }
290 storage: get, set, delete, keys — all return Result<T, StorageError>
291
292CREDENTIALS:
293 Declared in credentials {} block — host prompts user, injects at runtime
294 Access: api_key is a read-only binding in the space — NEVER put API keys in source
295
296UI COMPONENTS (record-style syntax):
297 Layout: Column { ... }, Row { ... }, Scroll { ... }
298 Content: Text { value: expr }, ProgressBar { value: 0.0-1.0 }
299 Interactive: Button { label: expr, on_tap: action_name }
300 TextInput { value: expr, on_change: action_name, placeholder: expr }
301 Data: ScrollList { items: expr, render: fn(item, index) { Component { ... } } }
302 Feedback: Modal { visible: bool, on_dismiss: action_name }, Toast { message: expr }
303 Conditional: if cond { Component { ... } }
304 List: for item in items { Component { ... } }
305
306RULES:
307 - Block order enforced: types→state→capabilities→credentials→derived→invariants→actions→views→update→handleEvent
308 - All state mutations use 'set' keyword, only inside actions
309 - Views are pure — no side effects, no set
310 - match must be exhaustive (cover all variants or use _)
311 - No imports, no file system, no globals — everything is in the space
312 - http responses are Result — always match Ok/Err or use ?
313 - tests {} block goes OUTSIDE the space, not inside
314 - Module names (math, core, time, etc.) are reserved — cannot shadow them
315 - list.of is special-cased variadic — no general variadic functions
316"#;
317
318pub fn generate_stdlib_table() -> String {
342 let reg = StdlibRegistry::new();
343 let descs = stdlib_descriptions();
344 let const_descs = constant_descriptions();
345
346 let mut total_functions = 0u32;
347 let mut total_constants = 0u32;
348 let mut modules_json = Vec::new();
349
350 for &module_name in MODULE_ORDER {
351 let mut funcs_json = Vec::new();
352 let mut consts_json = Vec::new();
353
354 if let Some(funcs) = reg.modules().get(module_name) {
356 let mut func_names: Vec<&String> = funcs.keys().collect();
357 func_names.sort();
358
359 for fname in func_names {
360 let sig = &funcs[fname];
361 let signature = format_signature(sig);
362 let desc = descs
363 .get(&(module_name, fname.as_str()))
364 .unwrap_or(&"");
365 funcs_json.push(format!(
366 r#" {{ "name": "{}", "signature": "{}", "variadic": {}, "description": "{}" }}"#,
367 fname,
368 escape_json(&signature),
369 sig.variadic,
370 escape_json(desc)
371 ));
372 total_functions += 1;
373 }
374 }
375
376 if let Some(consts) = reg.all_constants().get(module_name) {
378 let mut const_names: Vec<&String> = consts.keys().collect();
379 const_names.sort();
380
381 for cname in const_names {
382 let ty = &consts[cname];
383 let desc = const_descs
384 .get(&(module_name, cname.as_str()))
385 .unwrap_or(&"");
386 consts_json.push(format!(
387 r#" {{ "name": "{}", "type": "{}", "description": "{}" }}"#,
388 cname, ty, desc
389 ));
390 total_constants += 1;
391 }
392 }
393
394 modules_json.push(format!(
395 r#" {{
396 "name": "{}",
397 "functions": [
398{}
399 ],
400 "constants": [
401{}
402 ]
403 }}"#,
404 module_name,
405 funcs_json.join(",\n"),
406 consts_json.join(",\n"),
407 ));
408 }
409
410 format!(
411 r#"{{
412 "version": "{}",
413 "total_functions": {},
414 "total_constants": {},
415 "modules": [
416{}
417 ]
418}}"#,
419 crate::PEPL_LANGUAGE_VERSION,
420 total_functions,
421 total_constants,
422 modules_json.join(",\n"),
423 )
424}
425
426fn format_signature(sig: &crate::ty::FnSig) -> String {
428 let params: Vec<String> = sig
429 .params
430 .iter()
431 .map(|(name, ty)| {
432 if sig.variadic {
433 format!("...{}: {}", name, ty)
434 } else {
435 format!("{}: {}", name, ty)
436 }
437 })
438 .collect();
439 format!("({}) -> {}", params.join(", "), sig.ret)
440}
441
442fn escape_json(s: &str) -> String {
444 s.replace('\\', "\\\\")
445 .replace('"', "\\\"")
446 .replace('\n', "\\n")
447 .replace('\r', "\\r")
448 .replace('\t', "\\t")
449}
450
451#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn reference_is_non_empty() {
461 let reference = generate_reference();
462 assert!(!reference.is_empty());
463 assert!(reference.contains("PEPL: deterministic"));
464 assert!(reference.contains("STDLIB"));
465 }
466
467 #[test]
468 fn reference_contains_all_modules() {
469 let reference = generate_reference();
470 for &module in MODULE_ORDER {
471 assert!(
472 reference.contains(&format!(" {}:", module)),
473 "Reference missing module: {}",
474 module
475 );
476 }
477 }
478
479 #[test]
480 fn reference_contains_key_functions() {
481 let reference = generate_reference();
482 assert!(reference.contains("log"));
484 assert!(reference.contains("abs"));
485 assert!(reference.contains("length"));
486 assert!(reference.contains("map"));
487 assert!(reference.contains("filter"));
488 assert!(reference.contains("now"));
489 assert!(reference.contains("parse"));
490 }
491
492 #[test]
493 fn reference_contains_constants() {
494 let reference = generate_reference();
495 assert!(reference.contains("PI"));
496 assert!(reference.contains("E"));
497 }
498
499 #[test]
500 fn reference_token_estimate_under_2k() {
501 let reference = generate_reference();
502 let estimated_tokens = reference.len() / 4;
504 assert!(
505 estimated_tokens <= 2500,
506 "Reference is ~{} tokens (est.), should be ≤ 2K. Length: {} chars",
507 estimated_tokens,
508 reference.len()
509 );
510 }
511
512 #[test]
513 fn stdlib_table_is_valid_json() {
514 let table = generate_stdlib_table();
515 let parsed: serde_json::Value =
516 serde_json::from_str(&table).expect("stdlib table should be valid JSON");
517 assert!(parsed.is_object());
518 assert!(parsed["version"].is_string());
519 assert!(parsed["total_functions"].is_number());
520 assert!(parsed["total_constants"].is_number());
521 assert!(parsed["modules"].is_array());
522 }
523
524 #[test]
525 fn stdlib_table_has_all_modules() {
526 let table = generate_stdlib_table();
527 let parsed: serde_json::Value = serde_json::from_str(&table).unwrap();
528 let modules = parsed["modules"].as_array().unwrap();
529 assert_eq!(modules.len(), MODULE_ORDER.len());
530 for (i, &expected_name) in MODULE_ORDER.iter().enumerate() {
531 assert_eq!(modules[i]["name"].as_str().unwrap(), expected_name);
532 }
533 }
534
535 #[test]
536 fn stdlib_table_function_count() {
537 let table = generate_stdlib_table();
538 let parsed: serde_json::Value = serde_json::from_str(&table).unwrap();
539 let total = parsed["total_functions"].as_u64().unwrap();
540 assert!(
542 total >= 100,
543 "Expected at least 100 functions, got {}",
544 total
545 );
546 }
547
548 #[test]
549 fn stdlib_table_constant_count() {
550 let table = generate_stdlib_table();
551 let parsed: serde_json::Value = serde_json::from_str(&table).unwrap();
552 let total = parsed["total_constants"].as_u64().unwrap();
553 assert_eq!(total, 2, "Expected 2 constants (PI, E)");
554 }
555
556 #[test]
557 fn stdlib_table_functions_have_signatures() {
558 let table = generate_stdlib_table();
559 let parsed: serde_json::Value = serde_json::from_str(&table).unwrap();
560 let modules = parsed["modules"].as_array().unwrap();
561 for module in modules {
562 let funcs = module["functions"].as_array().unwrap();
563 for func in funcs {
564 assert!(
565 func["name"].is_string(),
566 "Function missing name in module {}",
567 module["name"]
568 );
569 assert!(
570 func["signature"].is_string(),
571 "Function {} missing signature",
572 func["name"]
573 );
574 let sig = func["signature"].as_str().unwrap();
575 assert!(
576 sig.contains("->"),
577 "Signature for {} should contain '->': {}",
578 func["name"],
579 sig
580 );
581 }
582 }
583 }
584
585 #[test]
586 fn stdlib_table_all_descriptions_present() {
587 let table = generate_stdlib_table();
588 let parsed: serde_json::Value = serde_json::from_str(&table).unwrap();
589 let modules = parsed["modules"].as_array().unwrap();
590 let mut missing = Vec::new();
591 for module in modules {
592 let module_name = module["name"].as_str().unwrap();
593 for func in module["functions"].as_array().unwrap() {
594 let fname = func["name"].as_str().unwrap();
595 let desc = func["description"].as_str().unwrap_or("");
596 if desc.is_empty() {
597 missing.push(format!("{}.{}", module_name, fname));
598 }
599 }
600 for con in module["constants"].as_array().unwrap() {
601 let cname = con["name"].as_str().unwrap();
602 let desc = con["description"].as_str().unwrap_or("");
603 if desc.is_empty() {
604 missing.push(format!("{}.{}", module_name, cname));
605 }
606 }
607 }
608 assert!(
609 missing.is_empty(),
610 "Missing descriptions for: {:?}",
611 missing
612 );
613 }
614
615 #[test]
616 fn stdlib_table_core_module_correct() {
617 let table = generate_stdlib_table();
618 let parsed: serde_json::Value = serde_json::from_str(&table).unwrap();
619 let core = &parsed["modules"][0];
620 assert_eq!(core["name"].as_str().unwrap(), "core");
621 let funcs = core["functions"].as_array().unwrap();
622 let names: Vec<&str> = funcs.iter().map(|f| f["name"].as_str().unwrap()).collect();
623 assert!(names.contains(&"log"));
624 assert!(names.contains(&"assert"));
625 assert!(names.contains(&"type_of"));
626 assert!(names.contains(&"capability"));
627 assert_eq!(funcs.len(), 4);
628 }
629
630 #[test]
631 fn stdlib_table_math_constants() {
632 let table = generate_stdlib_table();
633 let parsed: serde_json::Value = serde_json::from_str(&table).unwrap();
634 let math = &parsed["modules"][1];
635 assert_eq!(math["name"].as_str().unwrap(), "math");
636 let consts = math["constants"].as_array().unwrap();
637 assert_eq!(consts.len(), 2);
638 let names: Vec<&str> = consts.iter().map(|c| c["name"].as_str().unwrap()).collect();
639 assert!(names.contains(&"PI"));
640 assert!(names.contains(&"E"));
641 }
642
643 #[test]
644 fn reference_and_table_agree_on_modules() {
645 let reference = generate_reference();
646 let table = generate_stdlib_table();
647 let parsed: serde_json::Value = serde_json::from_str(&table).unwrap();
648 let modules = parsed["modules"].as_array().unwrap();
649 for module in modules {
650 let name = module["name"].as_str().unwrap();
651 assert!(
652 reference.contains(&format!(" {}:", name)),
653 "Reference missing module {} that table has",
654 name
655 );
656 }
657 }
658}