Sign in

Justin Garcia

@purefunctor.me
679 followers 482 following 238 posts

23, product and compiler engineering

PostsRepliesMedia
Justin Garcia @purefunctor.me · 18/06/2026
I added a rotary knob to configure the oklch hue on my website
030
Justin Garcia @purefunctor.me · 28/05/2026
I wonder if my project is being used for evals somewhere
GitHub insights graphs:

Clones in the last 14 days peaks at 2318
Unique cloners in the last 14 days peaks at 161
Total views in last 14 days peaks at 126
Unique visitors in last 14 days peaks at 6
140
Justin Garcia @purefunctor.me · 25/05/2026
I fear for my SSD health
cargo clean

Removed 739246 files, 90.7GiB total
020
Justin Garcia @purefunctor.me · 23/05/2026
used Amp's Rush 2.0 to write a super basic jj/git status for my starship setup
terminal powerline:

apple logo > .../alexandrte > qk (justin/constraint-solver) [❄️~1][🔖~0]
011
Justin Garcia @purefunctor.me · 21/05/2026
1 year of @kagi.com 💛
Kagi
Member since 05/2025
1110
Justin Garcia @purefunctor.me · 14/05/2026
delta / difft
150
Justin Garcia @purefunctor.me · 14/05/2026
oh jj
cargo clippy --fix, followed by jj absorb distributing the fixes into the correct revisions.
1200
Justin Garcia @purefunctor.me · 20/02/2026
that's a first
Amp Code transcript

No worries — restart and it should pick up the new permission rule. Try giving me a task after that and we'll see it in action!
let's do it again 2 + 2
$ say -v "Bubbles" "Two plus two is four. Mafs. Quick mafs. The calculator trembles before my power."
two plus two is four minus one that's...
3, quick maths! 🎤
$ say -v "Fred" "Three. Quick maths. Everyday mans on the block. Smoke trees."
000
Justin Garcia @purefunctor.me · 17/02/2026
you're telling me you can just do that in jj?
Terminal screenshot showing `jj log --limit 5`, the latest 5 commits; then `jj sign` which trivially adds a signature for all of them
1150
Justin Garcia @purefunctor.me · 29/01/2026
"However, we wish to stay close to programming practice and define patterns as follows" moscova.inria.fr/~maranget/pa...
Excerpt from http://moscova.inria.fr/~maranget/papers/warn/warn003.html

Patterns are used to discriminate amongst values. More precisely a pattern describes a set of values with a common prefix. That is, patterns are terms with variables and a given pattern p describes its instances σ(p) where σ ranges over substitutions. However, we wish to stay close to programming practice and define patterns as follows: 

p ::= Patterns
  _  wildcard
  c(p1,p2, … ,pa) constructed pattern a ≥ 0
  (p1 ∣ p2) or-pattern
130
Justin Garcia @purefunctor.me · 25/01/2026
my 2 days of GPT-5.2 xhigh so far
Hold up!! His writing is this fire???
101
Justin Garcia @purefunctor.me · 21/01/2026
Testing out progressive disclosure patterns for compiler testing tools with @ampcode.bsky.social
210
Justin Garcia @purefunctor.me · 19/01/2026
purescript-analyzer's playground now supports loading PureScript packages from the package set, check it out: purefunctor.github.io/purescript-a...
a screenshot of the purescript-analyzer playground, showing an example program and the type checker output
040
Justin Garcia @purefunctor.me · 16/01/2026
what do we have here 👀
"Internal Opus 4.5" by "Stealth" in the OpenCode dashboard.
000
Justin Garcia @purefunctor.me · 11/07/2025
I bought the vinyl!
000
Justin Garcia @purefunctor.me · 18/05/2025
I love when abstractions fall right into place
#[derive(Debug, PartialEq, Eq)]
pub enum Thing {
    Annotation(cst::Annotation),
    Expression(cst::Expression),
    Nothing,
}

pub fn thing_at_position(content: &str, parsed: ParsedModule, position: Position) -> Thing {
    let Some(offset) = position_to_offset(content, position) else {
        return Thing::Nothing;
    };

    let node = parsed.syntax_node();
    let token = node.token_at_offset(offset);

    match token {
        TokenAtOffset::None => Thing::Nothing,
        TokenAtOffset::Single(token) => thing_from_single(token),
        TokenAtOffset::Between(left, right) => thing_from_between(left, right),
    }
}

fn thing_from_single(token: SyntaxToken) -> Thing {
    token.parent_ancestors().find_map(thing_classify_node).unwrap_or(Thing::Nothing)
}

fn thing_classify_node(node: SyntaxNode) -> Option<Thing> {
    let kind = node.kind();
    if cst::Annotation::can_cast(kind) {
        cst::Annotation::cast(node).map(Thing::Annotation)
    } else if cst::Expression::can_cast(kind) {
        cst::Expression::cast(node).map(Thing::Expression)
    } else {
        None
    }
}

fn thing_from_between(left: SyntaxToken, right: SyntaxToken) -> Thing {
    let left = left.parent_ancestors().find_map(thing_classify_node).unwrap_or(Thing::Nothing);
    let right = right.parent_ancestors().find_map(thing_classify_node).unwrap_or(Thing::Nothing);

    match (&left, &right) {
        // If left/right share an ancestor
        (_, _) if left == right => left,
        (_, Thing::Nothing) => left,
        (Thing::Nothing, _) => right,
        // Otherwise, lean towards the right
        (_, _) => right,
    }
}
130
Justin Garcia @purefunctor.me · 17/05/2025
Now I gotta figure out how to make the build system parallel
Benchmark results for single core and multi core parsing the entire package set in the purescript-analyzer. 637ms on a single core, 108ms on 12 cores.
040
Justin Garcia @purefunctor.me · 16/05/2025
TIL that Option<NonZero<u32>> has the same size as NonZero<u32> and I'm wondering if it's a worthwhile optimisation to make for flattened ASTs? At 1 billion nodes yeah it'll save 12 GB but at a million 12 MB doesn't matter as much for modern hardware.
// size = 28 bytes
pub enum F {
    F1(Option<u32>, Option<u32>, Option<u32>),
    F2(Option<u32>, Option<u32>, Option<u32>),
}

// size = 16 bytes
pub enum G {
    G1(Option<NonZero<u32>>, Option<NonZero<u32>>, Option<NonZero<u32>>),
    G2(Option<NonZero<u32>>, Option<NonZero<u32>>, Option<NonZero<u32>>),
}

// 28 * 1_000_000 = 28_000_000 bytes
// 16 * 1_000_000 = 16_000_000 bytes
//
// 12_000_000 bytes -> 12 MB (does it matter?)
150
Justin Garcia @purefunctor.me · 15/05/2025
The purescript-analyzer currently hovers around 1.0~1.3 GiB of memory usage for 871k (!!!) lines of PureScript. A good chunk of the memory being allocated comes from the AST, specifically the mechanism it currently uses for flattening the representation.
btop screenshot showing 1.03 GiB memory usage for purescript-analyzer
040
Justin Garcia @purefunctor.me · 02/04/2025
been a hot minute since I cooked this good
fn explicit_module_exports(
    external: &mut impl External,
    state: &mut State,
    file: FileId,
    index: &Index,
    including_self: bool,
) {
    if including_self {
        implicit_module_exports(state, file, index);
    }

    for (name, id) in index.iter_term_nominal() {
        if index.is_term_exported(id) {
            state.add_term(name, file, id);
        }
    }

    for (name, id) in index.iter_type_nominal() {
        if index.is_type_exported(id) {
            state.add_type(name, file, id);
        }
    }

    for (_, items) in index.iter_import_items() {
        if !items.exported {
            continue;
        }
        let Some(name) = &items.name else {
            continue;
        };

        let import = external.file_id(name);
        let exports = external.exports(import);

        for (name, (file, id)) in exports.terms.iter() {
            state.add_term(name, *file, *id);
        }

        for (name, (file, id)) in exports.types.iter() {
            state.add_type(name, *file, *id);
        }
    }
}
150
Justin Garcia @purefunctor.me · 02/04/2025
types when you write _help / ?help:
"Th-this is my hole!"
"It was made for me!"
040
Justin Garcia @purefunctor.me · 11/03/2025
erm
Your system has run out of application memory.

Discord 958.9 MB
Ghostty 141.25 GB
Pages 430.9 MBB
Slack 6.58 GB
Zen 8.80 GB
Finder 84.5 MB
030
Justin Garcia @purefunctor.me · 02/03/2025
Instance heads are annoying to lower
            // TODO: Implement binding for Instance/InstanceBinder. Here are some cases to handle:
            //
            // instance Eq a
            //
            // instance TypeEq a a
            //
            // instance Ord a => Eq a
            //
            // We'll first encounter InstanceBinder when checking the instance head. If we were to
            // bind the names naively, we'll encounter a case where a name would have already been
            // bound (see TypeEq a a). We'll need to do some additional bookkeeping to manage
            // implicit foralls.
            //
            // The first time we see an InstanceBinder, we push it into the scope, giving us a de
            // Bruijn index. Subsequent usages of the InstanceBinder would simply return that de
            // Bruijn index. Likewise, any usage of Instance would refer back to that type
            // variable.
            //
            // Unlike explicitly-bound type variables, we have no identity to anchor to for
            // implicit type variables. As such, we must create them on the fly when lowering types
            // for instance heads. Although, we sort of do in a way? If we used an interner in the
            // Constraint graph node, we could obtain an ID that's specific to that scope. That
            // should be a sufficient anchor for de Bruijn indices _that is not_ SmolStr.
            //
            // Although, what if type variables didn't resolve to TypeVariableBindingId directly,
            // but to the scope nodes that they were allocated in? Then, we'll need to perform the
            // resolution _here_
000
Justin Garcia @purefunctor.me · 20/02/2025
Note to self: make sure the binary you're benchmarking bails if it doesn't find the files it's supposed to be using
Benchmark 1: ../target/release/compat
  Time (mean ± σ):     695.4 ms ±  16.9 ms    [User: 595.8 ms, System: 99.1 ms]
  Range (min … max):   676.5 ms … 725.6 ms    10 runs
120
Justin Garcia @purefunctor.me · 18/02/2025
GHC... seems to generate them?
(SK2) All skolemisation (even without DeepSubsumption) builds just one implication
      constraint for a nested forall like:
          forall a. Eq a => forall b. Ord b => blah
      The implication constraint will look like
          forall a b. (Eq a, Ord b) => <constraints>
      See the loop in GHC.Tc.Utils.Instantiate.topSkolemise.
      and Note [Skolemisation en-bloc] in that module
000
Justin Garcia @purefunctor.me · 18/02/2025
I was just thinking about how skolemisation would fit in a constraint-generating type checker and I was wondering if it makes sense to use implication constraints for them.
Slide about implication constraints from Simon Peyton Jones' ZuriHac 2019 Keynote

https://drive.google.com/file/d/1NRkP0hz-0Yo49Rto70b2nUwxjPiGD9Ci/view
100
Justin Garcia @purefunctor.me · 07/02/2025
Why do banks make such terrible software? Like I get hanging on to archaic systems but stuff like this is inexcusable
An alert box that states "User is already logged-in"
000
Justin Garcia @purefunctor.me · 05/02/2025
What'd I say about tiny interactions again
110
Justin Garcia @purefunctor.me · 03/02/2025
one of the takes of all time
Rust is a programming language, but it's also a way for Marxists to subvert the establishment and overthrow institutions.
1142
Justin Garcia @purefunctor.me · 01/02/2025
I love let-else
fn lower_expr_value(
    state: &mut State,
    module: &cst::Module,
    index: &IndexingResult,
    group: &ValueGroupId,
) {
    let root = module.syntax();

    let mut signature_id = None;
    'signature: {
        let Some(signature) = group.signature else { break 'signature };
        let Some(signature) = index.source_map.declaration_ptr(signature) else { break 'signature };
        let Some(signature) = signature.try_to_node(root) else { break 'signature };
        let cst::Declaration::ValueSignature(signature) = signature else { break 'signature };
        let Some(signature) = signature.signature() else { break 'signature };
        signature_id = Some(lower_type(state, &signature));
    };

    let capacity = group.equations.len();
    let mut equation_id = Vec::with_capacity(capacity);
    for equation in &group.equations {
        let Some(equation) = index.source_map.declaration_ptr(*equation) else { continue };
        let Some(equation) = equation.try_to_node(root) else { continue };
        let cst::Declaration::ValueEquation(equation) = equation else { continue };
        let Some(guarded) = equation.guarded_expression() else { continue };
        let Some(where_expression) = guarded.where_expression() else { continue };
        let Some(expression) = where_expression.expression() else { continue };
        equation_id.push(lower_expression(state, &expression));
    }

    dbg!((signature_id, equation_id));
}
180
Justin Garcia @purefunctor.me · 28/01/2025
I mean... parsing the entire package set seems to finish around 1.3ms
λ ~/pf/purescript-analyzer/ main* hyperfine -N --prepare "sync" --warmup 100  "./target/release/compat"
Benchmark 1: ./target/release/compat
  Time (mean ± σ):       1.3 ms ±   0.1 ms    [User: 0.6 ms, System: 0.4 ms]
  Range (min … max):     1.2 ms …   1.5 ms    76 runs
040
Justin Garcia @purefunctor.me · 26/01/2025
Any neovim plugin recommendations? The last time I used it extensively was 5 years ago
brew install neovim
1050
Justin Garcia @purefunctor.me · 23/01/2025
I'm just discovering now that a few months ago I wrote a build system that incidentally functions a lot like shakebuild.com I did know about the Build Systems a la Carte paper but it wasn't until today that I actually gave it a proper read-through (bad habit, working on it)
queryFns ∷ ∀ r. { | QueryFns r }
queryFns =
  { parsedFile: absurd
  , surfaceFull: surfaceFullImpl
  , surface: surfaceImpl
  , interface: interfaceImpl
  , scopeGraph: scopeGraphImpl
  , diagnostics: diagnosticsImpl
  }

surfaceFullImpl ∷ ∀ r. QueryFn r FileId SurfaceLower.Result
surfaceFullImpl engine id = do
  parsedFile ← inputGet @"parsedFile" engine id
  case parsedFile of
    ParsedTotal m →
      SurfaceLower.lowerModule m
    ParsedPartial m _ →
      SurfaceLower.lowerModule m

surfaceImpl ∷ ∀ r. QueryFn r FileId Module
surfaceImpl engine id =
  queryGet @"surfaceFull" engine id <#> _.surface

interfaceImpl ∷ ∀ r. QueryFn r FileId InterfaceCollect.Result
interfaceImpl engine id = do
  surface ← queryGet @"surface" engine id
  InterfaceCollect.collectInterface surface

scopeGraphImpl ∷ ∀ r. QueryFn r FileId ScopeCollect.Result
scopeGraphImpl engine id = do
  surface ← queryGet @"surface" engine id
  { interface } ← queryGet @"interface" engine id
  ScopeCollect.collectModule surface interface

diagnosticsImpl ∷ ∀ r. QueryFn r FileId (Set Diagnostic)
diagnosticsImpl engine id = do
  parsedFile ← inputGet @"parsedFile" engine id
  surfaceResult ← queryGet @"surfaceFull" engine id
  interfaceResult ← queryGet @"interface" engine id
  pure $ DiagnosticsCollate.collateDiagnostics parsedFile surfaceResult interfaceResult
120
Justin Garcia @purefunctor.me · 23/01/2025
I thought this felt familiar—isn't this the same mechanism that salsa-rs uses for dependency tracking?
5.2.2 Verifying Step Traces from https://simon.peytonjones.org/assets/pdfs/build-systems-jfp.pdf
020
Justin Garcia @purefunctor.me · 23/01/2025
absolute cinema
Build Systems a la Carte presentation slide by Simon Peyton Jones describing how the Task type's polymorphism can be used to extract static dependencies in a single line.
030
Justin Garcia @purefunctor.me · 18/01/2025
I live for tiny interactions like these
040
Justin Garcia @purefunctor.me · 18/01/2025
It's not often that I use function pointers to shared behaviour in Rust but when I do it ends up pretty cool.
    let (name_token, declaration, item_fn, make_fn) = match declaration {
        TypeDeclaration::Class(d) => (
            d.class_head().and_then(|h| h.name_token()),
            cst::Declaration::ClassDeclaration(d),
            TypeItem::class_group as TypeItemGroupFn,
            TypeItem::class_item as MakeTypeItemFn,
        ),
        TypeDeclaration::Data(e) => (
            e.name_token(),
            cst::Declaration::DataEquation(e),
            TypeItem::data_group as TypeItemGroupFn,
            TypeItem::data_item as MakeTypeItemFn,
        ),
        TypeDeclaration::Newtype(e) => (
            e.name_token(),
            cst::Declaration::NewtypeEquation(e),
            TypeItem::newtype_group as TypeItemGroupFn,
            TypeItem::newtype_item as MakeTypeItemFn,
        ),
        TypeDeclaration::Synonym(e) => (
            e.name_token(),
            cst::Declaration::TypeSynonymEquation(e),
            TypeItem::synonym_group as TypeItemGroupFn,
            TypeItem::synonym_item as MakeTypeItemFn,
        ),
    };
010
Justin Garcia @purefunctor.me · 13/01/2025
About time I did myself a favor!
Shipped Items: Keychron V10 Max with Gateron Jupiter Switches
050
Justin Garcia @purefunctor.me · 05/01/2025
I feel like compilers are good benchmark "thing to build" to determine what makes a language great—case in point, syntactic macros to generate a thousand lines of boilerplate code. Also throw in ADTs and exhaustive pattern matching in there since this macro creates those too.
create_ast_enum!(
    Expression
        | ExpressionTyped
        | ExpressionOperatorChain
        | ExpressionInfixChain
        | ExpressionTick
        | ExpressionNegate
        | ExpressionApplicationChain
        | ExpressionTypeArgument
        | ExpressionTermArgument
        | ExpressionIfThenElse
        | ExpressionLetIn
        | ExpressionLambda
        | ExpressionCaseOf
        | ExpressionDo
        | ExpressionAdo
        | ExpressionConstructor
        | ExpressionVariable
        | ExpressionOperatorName
        | ExpressionSection
        | ExpressionHole
        | ExpressionString
        | ExpressionChar
        | ExpressionTrue
        | ExpressionFalse
        | ExpressionInteger
        | ExpressionNumber
        | ExpressionArray
        | ExpressionRecord
        | ExpressionParenthesized
        | ExpressionRecordAccess
        | ExpressionRecordUpdate
);
240
Justin Garcia @purefunctor.me · 04/01/2025
I finally published PureScript Analyzer's Lossless CST demo purefunctor.github.io/purescript-a...
A screenshot of the PureScript Analyzer Lossless CST demo
050
Justin Garcia @purefunctor.me · 31/12/2024
Managed to catch a few nasty bugs in my parser by parsing the latest PureScript package set! My favorite one was when I accidentally made parsing for parenthesized types exponential because of how resilient parsers interact with choice between parsers
Ghostty showing logs of files being parsed—zero files remaining to be fixed.
040
Justin Garcia @purefunctor.me · 30/12/2024
I built a small interactive demo of PureScript Analyzer's resilient parser running on WebAssembly with SvelteKit!
2111
Justin Garcia @purefunctor.me · 29/12/2024
Added tests to assert that the parser is truly lossless and doesn't panic on missing tokens
test_each_file! { in "./crates/parsing/tests/parser" as lossless => |content: &str| {
    let lexed = lexing::lex(content);
    let tokens = lexing::layout(&lexed);
    let (node, _) = parsing::parse(&lexed, &tokens);
    assert_eq!(node.to_string(), content);
}}

test_each_file! { in "./crates/parsing/tests/parser" as stability => |content: &str| {
    let lexed = lexing::lex(content);
    for index in 0..lexed.kinds().len() - 1 {
        let partial = lexed.text_in_range(0..index + 1);
        let lexed = lexing::lex(partial);
        let tokens = lexing::layout(&lexed);
        let (node, _) = parsing::parse(&lexed, &tokens);
        assert_eq!(node.to_string(), partial);
    }
}}
030
Justin Garcia @purefunctor.me · 28/12/2024
In PureScript, bind and discard share a prefix in the form of binders and expressions e.g. value <- 123 pure value Determining if `value` is a binder or an expression requires unbounded lookahead for '<-', which is how I ended up writing this API for backtracking.
let c = p.checkpoint();

let bind = c.branch(p, do_statement_bind);
let discard = c.branch(p, do_statement_discard);

p.decide([bind, discard]);
200
Justin Garcia @purefunctor.me · 26/12/2024
mine were quite tame—although I remember this language having very wacky operators in place of keywords like `where`
By default, constructors are qualified by the types that they create.

justZero : List Int
justZero = List:Cons 0 List:Nil

However, the use declaration can be used to expose constructors without explicit qualification.

use List

justZero : List Int
justZero = Cons 0 Nil

justZero : List Int
justZero =
  let
    use List
  in
    Cons 0 Nil-- we use GADT syntax, but we don't _actually_ support GADTs
-- GADTs are hard to implement, and I'm not smart enough for them yet
List : Type -> Type
List a ?
  Nil : List a
  Cons : a -> List a -> List a
020
Justin Garcia @purefunctor.me · 26/12/2024
My website is now built with SvelteKit 😄
Home page of my website
1160
Justin Garcia @purefunctor.me · 26/12/2024
There's always a point in compiler development where the code I'm writing feels like it's doing chip damage and it just feels soooooooooooooo tedious
fn expression_5(p: &mut Parser) {
    let mut m = p.start();

    if p.at(SyntaxKind::IF) {
        expression_if_then_else(p, m);
    } else if p.at(SyntaxKind::LET) {
        expression_let(p, m);
    } else if p.at(SyntaxKind::BACKSLASH) {
        expression_lambda(p, m);
    } else if p.at(SyntaxKind::CASE) {
        expression_case(p, m);
    } else if p.at(SyntaxKind::DO) {
        expression_do(p, m);
    } else if p.at(SyntaxKind::ADO) {
        expression_ado(p, m);
    } else if p.at(SyntaxKind::PREFIX) && p.at_next(SyntaxKind::DO) {
        expression_do(p, m);
    } else if p.at(SyntaxKind::PREFIX) && p.at_next(SyntaxKind::ADO) {
        expression_ado(p, m);
    } else if p.at_in(EXPRESSION_ATOM_START) {
        expression_6(p);
        m.cancel(p);
    } else {
        m.cancel(p);
    }
}

fn expression_if_then_else(p: &mut Parser, mut m: NodeMarker) {
    p.expect(SyntaxKind::IF);
    expression(p);
    p.expect(SyntaxKind::THEN);
    expression(p);
    p.expect(SyntaxKind::ELSE);
    expression(p);
    m.end(p, SyntaxKind::ExpressionIfThenElse);
}

fn expression_let(p: &mut Parser, mut m: NodeMarker) {
    m.end(p, SyntaxKind::ExpressionLetIn);
}

fn expression_lambda(p: &mut Parser, mut m: NodeMarker) {
    m.end(p, SyntaxKind::ExpressionLambda);
}

fn expression_case(p: &mut Parser, mut m: NodeMarker) {
    m.end(p, SyntaxKind::ExpressionCaseOf);
}

fn expression_do(p: &mut Parser, mut m: NodeMarker) {
    m.end(p, SyntaxKind::ExpressionDo);
}

fn expression_ado(p: &mut Parser, mut m: NodeMarker) {
    m.end(p, SyntaxKind::ExpressionAdo);
}

fn expression_6(p: &mut Parser) {
    expression_7(p);
}
130
Justin Garcia @purefunctor.me · 20/12/2024
I should write a macro for this or something
parser_tests!(
    comment_pre_post => include_str!("parser/CommentPrePost.purs"),
    module_export => include_str!("parser/ModuleExport.purs"),
    module_export_class => include_str!("parser/ModuleExportClass.purs"),
    module_export_class_error_end => include_str!("parser/ModuleExportClassErrorEnd.purs"),
    module_export_comma_error => include_str!("parser/ModuleExportCommaError.purs"),
    module_export_data => include_str!("parser/ModuleExportData.purs"),
    module_export_data_comma_error => include_str!("parser/ModuleExportDataCommaError.purs"),
    module_export_data_item_after_double_period => include_str!("parser/ModuleExportDataItemAfterDoublePeriod.purs"),
    module_export_data_item_before_double_period => include_str!("parser/ModuleExportDataItemBeforeDoublePeriod.purs"),
    module_export_empty => include_str!("parser/ModuleExportEmpty.purs"),
    module_export_invalid => include_str!("parser/ModuleExportInvalid.purs"),
    module_export_type_operator => include_str!("parser/ModuleExportTypeOperator.purs"),
    module_export_type_operator_error => include_str!("parser/ModuleExportTypeOperatorError.purs"),
    module_export_type_operator_error_end => include_str!("parser/ModuleExportTypeOperatorErrorEnd.purs"),
    module_header => include_str!("parser/ModuleHeader.purs"),
    module_header_prefixed => include_str!("parser/ModuleHeaderPrefixed.purs"),
);
140
Justin Garcia @purefunctor.me · 20/12/2024
🙃
Warning from my service provider to not click links immediately followed by a spoofed phishing link.
020
Justin Garcia @purefunctor.me · 19/12/2024
I think one advantage of writing imperative parser code is being able to explicitly tell when something is continuing onto the next iteration or if it's breaking out
fn module_export_list(p: &mut Parser) {
    let mut marker = p.start();

    'list: {
        if p.at(SyntaxKind::LEFT_PARENTHESIS) {
            p.consume();

            if p.at(SyntaxKind::RIGHT_PARENTHESIS) {
                p.error("Empty export list");
                p.consume();
                break 'list;
            }

            let mut after_item = false;
            while !p.at(SyntaxKind::RIGHT_PARENTHESIS) && !p.at_eof() {
                if p.at_in(EXPORT_ITEM_START) {
                    module_export_item(p);
                    after_item = true;
                    continue;
                }
                if p.at(SyntaxKind::COMMA) {
                    if p.at_next(SyntaxKind::RIGHT_PARENTHESIS) {
                        p.error_recover("Trailing comma in export list");
                        continue;
                    }
                    if after_item {
                        p.consume();
                        after_item = false;
                        continue;
                    }
                    p.error_recover("Missing item in export list");
                    continue;
                }
                if p.at_in(EXPORT_LIST_RECOVERY) {
                    break;
                }
                p.error_recover("Invalid token in export list");
                after_item = true;
            }

            p.expect(SyntaxKind::RIGHT_PARENTHESIS);
        }
    }

    marker.end(p, SyntaxKind::ModuleExportList);
}
290