Skip to main content

logicaffeine_compile/
stdlib_text.rs

1//! The standard library's text, read as the prelude reads it: pure functions over a module's source, with no
2//! dependency beyond `std`.
3//!
4//! The crate's build script compiles this file too (`#[path]`) and runs [`index`] once over the embedded modules,
5//! writing what every process would otherwise derive at startup — each module's names and natives, which modules
6//! its code pulls in, the bytes its splice keeps and where each library function's section lies — as statics the
7//! loader reads ([`crate::loader`]). A process that splices nothing reads none of the library's text.
8
9/// Every stdlib module that auto-imports, by file stem under `assets/std/`, in splice order. The build script
10/// embeds each as `STD_<STEM>` and lists them, in this order, as the loader's `PRELUDE_MODULES`. The trigger
11/// identifiers and collision keys are not hand-maintained — they are derived from each module's own definitions
12/// ([`defined_names`]), so adding a module here makes its whole vocabulary live with nothing else to update.
13/// `std.lg` is absent: its `Unit`, `Pair` and `Triple` stay an explicit import (`logos:std`).
14#[cfg_attr(not(test), allow(dead_code))]
15pub(crate) const PRELUDE_FILES: [&str; 46] = [
16    // Core algebraic types (Result/Option) come first: later modules'
17    // signatures (file.lg's `-> Result of Text and Text`) refer to them.
18    "core",
19    "concurrency",
20    "net",
21    "io",
22    "crdt",
23    "env",
24    "file",
25    "tcp",
26    "web",
27    "session",
28    "postgres",
29    "random",
30    "time",
31    "http",
32    "static",
33    "crypto",
34    "uuid",
35    "media",
36    "math",
37    "strings",
38    // After `strings`: `forms.lg` validates through its `trim`.
39    "forms",
40    "ui",
41    "iter",
42    "convert",
43    "error",
44    "rng",
45    "regex",
46    "json",
47    "encoding",
48    "compress",
49    "bits",
50    "unicode",
51    "operator",
52    "bytes",
53    "chars",
54    "statistics",
55    "builder",
56    "collections",
57    "heap",
58    "sort",
59    "calendar",
60    "blas",
61    "lapack",
62    "clapack",
63    "vision",
64    "vdom",
65];
66
67/// What the prelude needs of its modules, derived from their text alone ([`index`]).
68#[cfg_attr(not(test), allow(dead_code))]
69pub(crate) struct StdlibIndex {
70    /// Per module, the names it defines ([`module_names`]).
71    pub names: Vec<Vec<String>>,
72    /// Per module, the names its spliced code declares `## To native` ([`module_native_names`]).
73    pub natives: Vec<Vec<String>>,
74    /// Per module, every other module with a name that is not builtin-backed and that its spliced code uses
75    /// ([`references`]): splicing the module splices these too.
76    pub pulls: Vec<Vec<usize>>,
77    /// Per module, whether its spliced code mentions `text_from_bytes`.
78    pub mentions_text_from_bytes: Vec<bool>,
79    /// Per module, the byte ranges of its source whose concatenation is its spliced code ([`spliced_code`]).
80    pub splice: Vec<Vec<(usize, usize)>>,
81    /// Every name a module's code opens a `## To` header line with, at any indentation, sorted: the sections a
82    /// pruner may drop.
83    pub functions: Vec<String>,
84    /// Every `## To` section of the spliced modules as `(name, module, start, end)`, the section being
85    /// `module[start..end]` of the module's source; sorted by name, and a name two modules define keeps the later
86    /// module's section.
87    pub library: Vec<(String, usize, usize, usize)>,
88}
89
90/// A module's code as the prelude splices it: its `## Note` prose and its builtin-backed declarations stripped.
91pub(crate) fn spliced_code(source: &str) -> String {
92    strip_builtin_backed_decls(&strip_note_blocks(module_code(source)))
93}
94
95/// Derive the [`StdlibIndex`] of `modules`, given in splice order. Panics when a module's spliced code is not a
96/// sequence of its source's lines, or a library section is not one run of its source: either would leave the
97/// index unable to name the bytes, and the build must stop rather than embed an index that disagrees with the text.
98#[cfg_attr(not(test), allow(dead_code))]
99pub(crate) fn index(modules: &[&str]) -> StdlibIndex {
100    let spliced: Vec<String> = modules.iter().map(|m| spliced_code(m)).collect();
101    let names: Vec<Vec<String>> = modules.iter().map(|m| module_names(m)).collect();
102    let natives = spliced
103        .iter()
104        .map(|code| module_native_names(code))
105        .collect();
106    let pulls = spliced
107        .iter()
108        .enumerate()
109        .map(|(j, code)| {
110            let words = whole_words(code);
111            (0..modules.len())
112                .filter(|&k| {
113                    k != j
114                        && names[k].iter().any(|n| {
115                            !builtin_backed(n) && may_name(&words, n) && references(code, n)
116                        })
117                })
118                .collect()
119        })
120        .collect();
121    let mentions_text_from_bytes = spliced
122        .iter()
123        .map(|code| code.contains("text_from_bytes"))
124        .collect();
125    let splice: Vec<Vec<(usize, usize)>> = modules
126        .iter()
127        .zip(&spliced)
128        .enumerate()
129        .map(|(m, (source, code))| {
130            let runs = line_runs(source, code).unwrap_or_else(|| {
131                panic!("module {m}: its spliced code is not a sequence of its source's lines")
132            });
133            assert_eq!(
134                runs.iter().map(|&(s, e)| &source[s..e]).collect::<String>(),
135                *code,
136                "module {m}: the splice's runs do not rebuild its spliced code"
137            );
138            runs
139        })
140        .collect();
141    let mut by_name: std::collections::BTreeMap<String, (usize, usize, usize)> =
142        std::collections::BTreeMap::new();
143    for (m, code) in spliced.iter().enumerate() {
144        for (name, range) in function_section_ranges(code) {
145            let (start, end) = source_range(&splice[m], range.clone()).unwrap_or_else(|| {
146                panic!("module {m}: `## To {name}` spans bytes its splice strips, so no one run of the source holds it")
147            });
148            assert_eq!(
149                &modules[m][start..end],
150                &code[range],
151                "module {m}: `## To {name}` maps to other bytes"
152            );
153            by_name.insert(name, (m, start, end));
154        }
155    }
156    let library = by_name
157        .into_iter()
158        .map(|(name, (m, s, e))| (name, m, s, e))
159        .collect();
160    let functions = modules
161        .iter()
162        .flat_map(|source| {
163            module_code(source)
164                .lines()
165                .filter_map(|l| fn_section_name(&format!("{l}\n")))
166        })
167        .collect::<std::collections::BTreeSet<String>>()
168        .into_iter()
169        .collect();
170    StdlibIndex {
171        names,
172        natives,
173        pulls,
174        mentions_text_from_bytes,
175        splice,
176        functions,
177        library,
178    }
179}
180
181/// The byte runs of `source` whose concatenation is `code`, when `code` is a sequence of `source`'s lines (the last
182/// one possibly without its newline), taken earliest first and adjacent lines merged into one run.
183fn line_runs(source: &str, code: &str) -> Option<Vec<(usize, usize)>> {
184    let mut runs: Vec<(usize, usize)> = Vec::new();
185    let mut wanted = code.split_inclusive('\n').peekable();
186    let mut at = 0;
187    for line in source.split_inclusive('\n') {
188        let start = at;
189        at += line.len();
190        let Some(&want) = wanted.peek() else { break };
191        let taken = if line == want {
192            line.len()
193        } else if wanted.clone().nth(1).is_none() && line.strip_suffix('\n') == Some(want) {
194            want.len()
195        } else {
196            continue;
197        };
198        wanted.next();
199        match runs.last_mut() {
200            Some(run) if run.1 == start => run.1 = start + taken,
201            _ => runs.push((start, start + taken)),
202        }
203    }
204    wanted.peek().is_none().then_some(runs)
205}
206
207/// Where `range` of the code `runs` rebuild lies in the source, when it falls inside one run.
208fn source_range(runs: &[(usize, usize)], range: std::ops::Range<usize>) -> Option<(usize, usize)> {
209    let mut code_at = 0;
210    for &(s, e) in runs {
211        let len = e - s;
212        if range.start >= code_at && range.end <= code_at + len {
213            return Some((s + range.start - code_at, s + range.end - code_at));
214        }
215        code_at += len;
216    }
217    None
218}
219
220/// Every maximal run of identifier bytes in `text`: the only places [`references`] can find a name made of
221/// identifier bytes.
222pub(crate) fn whole_words(text: &str) -> std::collections::HashSet<&str> {
223    text.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
224        .filter(|w| !w.is_empty())
225        .collect()
226}
227
228/// Whether a whole-word scan of a text whose [`whole_words`] are `words` can find `name`. A name of identifier
229/// bytes occurs as a whole word only as one of the text's words; any other name is left to the scan.
230pub(crate) fn may_name(words: &std::collections::HashSet<&str>, name: &str) -> bool {
231    !name.bytes().all(is_ident_byte) || words.contains(name)
232}
233
234/// The CODE of a module — from its first `##` section onward, dropping the
235/// markdown title + leading prose. Literate Logos only skips prose *before* the
236/// first section, so when modules are concatenated only the leading prose is a
237/// hazard; stripping it makes the join parse cleanly. Documentation prose stays
238/// in the source files.
239pub(crate) fn module_code(md: &str) -> &str {
240    if let Some(i) = md.find("\n## ") {
241        &md[i + 1..]
242    } else if md.starts_with("## ") {
243        md
244    } else {
245        ""
246    }
247}
248
249/// A module's code with `## Note` documentation blocks removed — what the
250/// prelude actually prepends. Notes are the IDE's per-definition doc carrier
251/// (see `prelude_module_sources`); the runtime prelude stays lean, note-free,
252/// and byte-identical to the pre-documentation join.
253///
254/// A note is EVERYTHING from its `## Note` line to the next `##` header or the next
255/// bare type declaration (`A Request has:` inside a `## Definition` body, core.lg's
256/// `A Result of [T] and [E] is either:`) — several paragraphs when the author wrote a
257/// one-line summary followed by details (the IDE leads hover with the summary). A
258/// blank line never ends a note: it separates paragraphs OF the note. Locked by
259/// `note_stripping_removes_every_paragraph_of_a_note` — a stripper that stopped at
260/// the first blank line leaked every later paragraph into the prelude as prose.
261pub(crate) fn strip_note_blocks(code: &str) -> String {
262    let mut out = String::with_capacity(code.len());
263    let mut in_note = false;
264    for line in code.split_inclusive('\n') {
265        let trimmed = line.trim();
266        if in_note {
267            let header = trimmed.starts_with("## ") && trimmed != "## Note";
268            if header || is_bare_type_declaration(line) {
269                in_note = false;
270                out.push_str(line);
271            }
272            continue;
273        }
274        if trimmed == "## Note" {
275            in_note = true;
276            continue;
277        }
278        out.push_str(line);
279    }
280    out
281}
282
283/// A top-level bare type declaration — the one non-`##` line kind that can follow a note:
284/// `A Name has:`, `A Name of [T] and [E] is either:`, `A Name is Shared and has:`,
285/// `A Name is a record.`, `A Name is a generic collection.` (column 0, `A`/`An`, a
286/// Capitalized name, optional `of [..]`/`and [..]` binders, then the declaring verb).
287/// Prose that merely opens with an article and a capitalized word (`A StringBuilder
288/// accumulates …`, `An HTTP response …: …`) is not one.
289fn is_bare_type_declaration(line: &str) -> bool {
290    if line.starts_with(' ') || line.starts_with('\t') {
291        return false;
292    }
293    let line = line.trim_end();
294    let Some(rest) = line.strip_prefix("An ").or_else(|| line.strip_prefix("A ")) else {
295        return false;
296    };
297    let mut words = rest.split_whitespace();
298    let Some(name) = words.next() else {
299        return false;
300    };
301    if !name.chars().next().is_some_and(|c| c.is_ascii_uppercase())
302        || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
303    {
304        return false;
305    }
306    let tail: Vec<&str> = words.collect();
307    // Skip the generic binders: `of [T]`, `and [E]`, `of [K] and [V]`.
308    let mut i = 0;
309    while i + 1 < tail.len() && matches!(tail[i], "of" | "and") && tail[i + 1].starts_with('[') {
310        i += 2;
311    }
312    let verb = tail[i..].join(" ");
313    verb == "has:"
314        || verb == "is either:"
315        || verb == "is Shared and has:"
316        || (verb.starts_with("is a ") && verb.ends_with('.'))
317}
318
319/// The article-led declaration a trimmed line opens — `A Name …` / `An Name …`, in a block or on its
320/// own section header (`## A Response has:`) — as the text after the article and whether the line was
321/// a `##` header. A section header only ever declares a type, never a variant.
322fn article_declaration(t: &str) -> Option<(&str, bool)> {
323    let (declaration, on_header) = match t.strip_prefix("## ") {
324        Some(rest) => (rest, true),
325        None => (t, false),
326    };
327    let rest = declaration
328        .strip_prefix("A ")
329        .or_else(|| declaration.strip_prefix("An "))?;
330    Some((rest, on_header))
331}
332
333/// Builtin-backed names can never DEMAND an import — the engine resolves
334/// `List`/`Map` without the prelude, and `Option` is constructed by the
335/// `some x`/`none` LITERALS (codegen lowers them to `std::option::Option`;
336/// a prepended enum declaration would emit a clashing `user_types::Option`).
337/// Letting any of them trigger would also change collection-using programs'
338/// expanded bytes for no semantic gain. (They still count on the collision
339/// side: a user-declared `Map` type suppresses the module, declarer-wins as
340/// everywhere else.)
341pub(crate) fn builtin_backed(name: &str) -> bool {
342    matches!(name, "List" | "Map" | "Option" | "Some" | "None")
343}
344
345/// Drop a module's declaration lines for builtin-backed types before
346/// prepending. `A Map of [K] and [V] is a generic collection.` is
347/// documentation of a builtin — REGISTERING it would shadow the builtin's own
348/// `Map of K to V` syntax with the user-generic `of … and …` form and break
349/// every `Map of Text to Int` annotation downstream. The engine resolves these
350/// types natively; the prelude must stay silent about them.
351pub(crate) fn strip_builtin_backed_decls(code: &str) -> String {
352    let mut out: Vec<&str> = Vec::new();
353    let mut in_stripped_enum = false;
354    for line in code.lines() {
355        let t = line.trim();
356        let indented = line.starts_with(' ') || line.starts_with('\t');
357        if in_stripped_enum {
358            // A stripped enum header owns its indented variant block.
359            if indented || t.is_empty() {
360                continue;
361            }
362            in_stripped_enum = false;
363        }
364        if let Some((rest, _)) = article_declaration(t) {
365            if let Some(word) = rest
366                .split(|c: char| c == '(' || c == '.' || c.is_whitespace())
367                .next()
368            {
369                if !indented && builtin_backed(word) {
370                    in_stripped_enum = t.ends_with("is either:");
371                    continue;
372                }
373            }
374        }
375        out.push(line);
376    }
377    out.join("\n")
378}
379
380/// The names a module's CODE defines, used both as auto-import triggers and collision keys
381/// ("declarer wins"): helper / native procedures (`## To [native] <name>`) and the names of
382/// *type definitions* (`A <Name> has …` / `A <Name> is …` / `A <Name> of [T] …`).
383///
384/// Enum variant constructors (`A Debug.`, `A Some (value: T).`, `A Failure with msg Text.`)
385/// are names too: constructing `a new Ok with value …` or matching `When Ok (v):` must pull in
386/// the module that declares the enum, and a program's own variant of the same name must be
387/// recognized as the program's. A bare mention in prose never triggers — [`references`] only
388/// counts use positions. Field lines (`a sender, which is Int.`) are lowercase and skip.
389pub(crate) fn defined_names(code: &str) -> Vec<String> {
390    defined_names_with_visibility(code)
391        .into_iter()
392        .map(|(n, _)| n)
393        .collect()
394}
395
396/// Like `defined_names`, but also reports whether each definition is `private`
397/// (declared `## To private …` / `## A private …`). Public-by-default: a name with
398/// no `private` marker is `false`. Powers module encapsulation — a private name is
399/// usable by its owning module but not importable by any other file. Public so the
400/// language server can filter private names out of its cross-module surfaces
401/// (completion, auto-import, workspace symbols) from the one source of truth.
402pub fn defined_names_with_visibility(code: &str) -> Vec<(String, bool)> {
403    let mut names = Vec::new();
404    // Inside an `is either:` block every indented line is a variant, and a variant's article is
405    // optional (`Failure (message: Text).` declares `Failure` exactly as `A Failure (…).` does).
406    let mut in_either_block = false;
407    for line in code.lines() {
408        let t = line.trim();
409        let indented = line.starts_with(' ') || line.starts_with('\t');
410        if !indented && !t.is_empty() {
411            in_either_block = false;
412        }
413        if in_either_block && indented && !t.starts_with("A ") && !t.starts_with("An ") {
414            if let Some(word) = t
415                .split(|c: char| c == '(' || c == '.' || c.is_whitespace())
416                .next()
417            {
418                let tail = t[word.len()..].trim_start();
419                let is_variant = tail.starts_with('(') || tail == "." || tail.starts_with("with");
420                if is_variant && word.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
421                    names.push((word.to_string(), false));
422                }
423            }
424            continue;
425        }
426        if !indented && t.ends_with("is either:") {
427            in_either_block = true;
428        }
429        if t.starts_with("## To ") {
430            if let Some(header) = function_header(t) {
431                names.push((header.name.to_string(), header.private));
432            }
433            continue;
434        }
435        if let Some((rest, on_header)) = article_declaration(t) {
436            let (rest, private) = match rest.strip_prefix("private ") {
437                Some(r) => (r, true),
438                None => (rest, false),
439            };
440            if let Some(word) = rest
441                .split(|c: char| c == '(' || c == '.' || c.is_whitespace())
442                .next()
443            {
444                if word.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
445                    // A type *header* continues with `has` / `is` / `of`. A bare
446                    // `A Name.` / `A Name (fields).` is a variant constructor —
447                    // ALSO a name: constructing `a new Ok with value …` or
448                    // matching `When Ok (v):` must pull in the module that
449                    // declares the enum (core.lg's Result/Option), and the same
450                    // rule on the user side keeps "declarer wins" symmetric — a
451                    // user enum with an `Ok` variant suppresses the prelude's.
452                    let tail = rest[word.len()..].trim_start();
453                    let is_type_header =
454                        tail.starts_with("has") || tail.starts_with("is") || tail.starts_with("of");
455                    // Variants in BOTH spellings — concise `A Ok (value: T).` /
456                    // bare `A None.` AND natural `A Failure with msg Text.` —
457                    // otherwise declarer-wins is asymmetric: a user's natural-
458                    // form `Failure` variant would fail to suppress io.lg's.
459                    let is_variant =
460                        tail.starts_with('(') || tail == "." || tail.starts_with("with");
461                    if is_type_header || (is_variant && !on_header) {
462                        names.push((word.to_string(), private));
463                    }
464                }
465            }
466        }
467    }
468    names
469}
470
471/// The names a prelude module owns (derived from its CODE, notes stripped —
472/// documentation prose must never mint a trigger name).
473pub(crate) fn module_names(src: &str) -> Vec<String> {
474    defined_names(&strip_note_blocks(module_code(src)))
475}
476
477/// The names a module declares `## To native …` — bound to a runtime implementation BY NAME on
478/// every tier, so hygiene can never rename one (see [`crate::loader::apply_prelude_mapped`]).
479pub(crate) fn module_native_names(code: &str) -> Vec<String> {
480    code.lines()
481        .filter_map(function_header)
482        .filter(|header| header.native)
483        .map(|header| header.name.to_string())
484        .collect()
485}
486
487pub(crate) fn is_ident_byte(b: u8) -> bool {
488    b.is_ascii_alphanumeric() || b == b'_'
489}
490
491/// Does `source` USE `name` — call it, launch it, or name it as a type? We require `name`
492/// to appear as a whole word in a *use position* (immediately called `name(`, or preceded
493/// by an invocation/type keyword like `a`/`an`/`the`/`new`/`of`/`to`/`Call` or a `:`), so a
494/// bare mention in prose, a string literal, or a larger identifier never drags the module
495/// in. This is what lets the auto-import stay invisible without false positives.
496pub(crate) fn references(source: &str, name: &str) -> bool {
497    if name.is_empty() {
498        return false;
499    }
500    let sb = source.as_bytes();
501    let mut search_from = 0;
502    while let Some(off) = source[search_from..].find(name) {
503        let start = search_from + off;
504        let end = start + name.len();
505        search_from = start + 1;
506        // Whole word: the name must not be part of a larger identifier.
507        if start > 0 && is_ident_byte(sb[start - 1]) {
508            continue;
509        }
510        if end < sb.len() && is_ident_byte(sb[end]) {
511            continue;
512        }
513        // Call form: `name(`.
514        if end < sb.len() && sb[end] == b'(' {
515            return true;
516        }
517        // Use position: preceded ON THE SAME LINE by an invocation/type keyword, a `:`
518        // (param/field type), or a `->` (return type — `-> Result of Int and Text` is as
519        // much a use of `Result` as a parameter annotation is). `When` covers match arms
520        // on a module's enum variants (`When Ok (v):`). The licensing token must share
521        // the name's line: a `has:` header colon or a trailing `to` that merely ENDS the
522        // previous line says nothing about the first word of the next one — otherwise a
523        // struct FIELD named like a stdlib function (`rank: Divergent Int` under
524        // `A Page is Shared and has:`) would splice that module in (see the unit locks).
525        let prefix = source[..start].trim_end();
526        let gap_spans_a_line = source[prefix.len()..start].contains('\n');
527        if gap_spans_a_line {
528            continue;
529        }
530        // A template FILTER is a call written left to right: `{price | money | pad 8}` uses
531        // `money` and `pad` exactly as `money(price)` would, so the demand scan must see it.
532        if prefix.ends_with(':') || prefix.ends_with("->") || prefix.ends_with('|') {
533            return true;
534        }
535        let last_word = prefix
536            .rsplit(|c: char| c.is_whitespace())
537            .next()
538            .unwrap_or("");
539        if matches!(
540            last_word,
541            "a" | "an" | "the" | "new" | "of" | "to" | "Call" | "When"
542        ) {
543            return true;
544        }
545    }
546    false
547}
548
549/// A `## To …` function header: the name it defines and its modifiers.
550pub(crate) struct FunctionHeader<'a> {
551    pub name: &'a str,
552    pub native: bool,
553    pub private: bool,
554}
555
556/// Read `line` as a `## To …` function header. The modifiers `native` and `private` come in any
557/// order; the name ends at the first `(` (the parameters), `:` (a header with none, `## To worker:`),
558/// or whitespace (`## To size of [T] …`). The one header reader every name-level pass shares, so a
559/// parameterless function is `worker` everywhere, never `worker:` in one pass and `worker` in another.
560pub(crate) fn function_header(line: &str) -> Option<FunctionHeader<'_>> {
561    let mut rest = line.trim().strip_prefix("## To ")?;
562    let (mut native, mut private) = (false, false);
563    loop {
564        if let Some(r) = rest.strip_prefix("native ") {
565            native = true;
566            rest = r;
567        } else if let Some(r) = rest.strip_prefix("private ") {
568            private = true;
569            rest = r;
570        } else {
571            break;
572        }
573    }
574    let name = rest
575        .split(|c: char| c == '(' || c == ':' || c.is_whitespace())
576        .next()?;
577    (!name.is_empty()).then_some(FunctionHeader {
578        name,
579        native,
580        private,
581    })
582}
583
584/// The name a `## To <name> …` section defines, `None` for any other section
585/// (`## A` struct, `## Main`, prose).
586pub(crate) fn fn_section_name(section: &str) -> Option<String> {
587    function_header(section.lines().next()?).map(|header| header.name.to_string())
588}
589
590/// Split code into its `## To` function sections: `(name, range)`, the section running from its header to the next
591/// `## ` header, trimmed, as a byte range of `code`.
592pub(crate) fn function_section_ranges(code: &str) -> Vec<(String, std::ops::Range<usize>)> {
593    let trimmed = |start: usize, end: usize| {
594        let text = &code[start..end];
595        let lead = text.len() - text.trim_start().len();
596        start + lead..start + lead + text.trim().len()
597    };
598    let mut out = Vec::new();
599    let mut current: Option<(String, usize)> = None;
600    let mut at = 0;
601    for line in code.split_inclusive('\n') {
602        if line.starts_with("## ") {
603            if let Some((name, start)) = current.take() {
604                out.push((name, trimmed(start, at)));
605            }
606            if let Some(name) = fn_section_name(line) {
607                current = Some((name, at));
608            }
609        }
610        at += line.len();
611    }
612    if let Some((name, start)) = current {
613        out.push((name, trimmed(start, code.len())));
614    }
615    out
616}
617
618#[cfg(test)]
619#[path = "../../logicaffeine_tests/unit/logicaffeine_compile/stdlib_text/tests.rs"]
620mod tests;