Skip to main content

pepl_compiler/
reference.rs

1//! Machine-generated PEPL reference and stdlib table.
2//!
3//! Produces two text artifacts from [`StdlibRegistry`]:
4//! 1. **Compressed PEPL reference** (~2K tokens) for LLM context injection
5//! 2. **Structured stdlib table** (JSON) for tooling and documentation
6//!
7//! Both auto-update when the stdlib registry changes — they are generated,
8//! not hand-written.
9
10use std::collections::BTreeMap;
11
12use crate::stdlib::StdlibRegistry;
13
14// ══════════════════════════════════════════════════════════════════════════════
15// Module ordering and descriptions
16// ══════════════════════════════════════════════════════════════════════════════
17
18/// Canonical module ordering for output stability.
19const 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
35/// Build a description map for all stdlib functions.
36/// Key: `(module, function)`, value: human-readable description.
37fn stdlib_descriptions() -> BTreeMap<(&'static str, &'static str), &'static str> {
38    let mut d = BTreeMap::new();
39
40    // ── core ──
41    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    // ── math ──
47    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    // ── string ──
59    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    // ── list ──
81    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    // ── record ──
117    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    // ── time ──
124    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    // ── convert ──
131    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    // ── json ──
138    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    // ── timer ──
142    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    // ── http (capability) ──
148    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    // ── storage (capability) ──
155    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    // ── location (capability) ──
161    d.insert(("location", "current"), "Get current location as { lat: number, lon: number }");
162
163    // ── notifications (capability) ──
164    d.insert(("notifications", "send"), "Send a notification with title and body");
165
166    d
167}
168
169/// Constant descriptions: `(module, name)` → description.
170fn 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
177// ══════════════════════════════════════════════════════════════════════════════
178// Compressed Reference Generation
179// ══════════════════════════════════════════════════════════════════════════════
180
181/// Generate the compressed PEPL reference (~2K tokens) for LLM context injection.
182///
183/// The STDLIB section is machine-generated from the [`StdlibRegistry`].
184/// All other sections are static text matching the format in `llm-generation-contract.md`.
185pub fn generate_reference() -> String {
186    let reg = StdlibRegistry::new();
187    let mut out = String::with_capacity(4096);
188
189    // Static preamble
190    out.push_str(REFERENCE_PREAMBLE);
191
192    // Dynamic STDLIB section
193    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            // Also include constants for this module
200            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    // Static postamble
218    out.push_str(REFERENCE_POSTAMBLE);
219
220    out
221}
222
223/// The static preamble of the compressed reference (everything before STDLIB).
224const 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
283/// The static postamble of the compressed reference (everything after STDLIB).
284const 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
318// ══════════════════════════════════════════════════════════════════════════════
319// Stdlib Table Generation (JSON)
320// ══════════════════════════════════════════════════════════════════════════════
321
322/// Generate a structured JSON stdlib table for tooling and documentation.
323///
324/// Output format:
325/// ```json
326/// {
327///   "version": "0.1.0",
328///   "total_functions": 100,
329///   "total_constants": 2,
330///   "modules": [
331///     {
332///       "name": "core",
333///       "functions": [
334///         { "name": "log", "signature": "(value: any) -> nil", "description": "..." }
335///       ],
336///       "constants": []
337///     }
338///   ]
339/// }
340/// ```
341pub 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        // Functions
355        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        // Constants
377        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
426/// Format a function signature as `(param: type, ...) -> return_type`.
427fn 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
442/// Minimal JSON string escaping.
443fn 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// ══════════════════════════════════════════════════════════════════════════════
452// Tests
453// ══════════════════════════════════════════════════════════════════════════════
454
455#[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        // Spot-check some key functions appear in the STDLIB section
483        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        // Rough token estimate: ~4 chars per token for English text
503        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        // The registry has functions for all 13 modules
541        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}