Skip to content
LinkState
Go back

Generating parsers from samples without blind trust

#Comparing Grammar Approaches for Platform Onboarding
Hand‑written grammars, parser‑generator libraries, and LLM‑generated extraction rules


Hand‑Written Grammars

Overview

Hand‑written grammars are explicit specifications of a language’s syntax authored directly by a developer, typically expressed in Backus‑Naur Form (BNF), Extended BNF (EBNF), or a custom DSL. The grammar defines terminals, non‑terminals, production rules, and optionally semantic actions. For platform onboarding, the grammar describes the structure of configuration files, API payloads, or CLI output that must be parsed before the data can be ingested into automation workflows.

Advantages & Disadvantages

AdvantagesDisadvantages
Full control – every rule can be tuned for edge cases, whitespace handling, or platform‑specific quirks.High authoring effort – designing a correct grammar for a complex format (e.g., JSON‑like with comments) can take days.
Predictable performance – hand‑crafted parsers can be optimized (e.g., recursive‑descent with memoization) to achieve linear time.Maintenance burden – any change in the source format requires manual grammar updates and regression testing.
No external dependencies – the parser can be compiled into a static binary, simplifying deployment in air‑gapped environments.Error‑prone – subtle ambiguities or left‑recursion bugs are easy to introduce and hard to detect without extensive testing.
Deterministic semantics – semantic actions are explicit, making it straightforward to map parsed nodes to internal data models.Limited reuse – grammars are often tightly coupled to a specific language, reducing sharing across teams.

Example Use Cases


Parser‑Generator Libraries

Overview

Parser‑generator libraries take a formal grammar description (usually EBNF‑like) and automatically produce a parser (often recursive‑descent, LR, or PEG) in the target language. The developer writes the grammar once; the generator emits source code or a runtime parser object. This approach reduces manual parser implementation while preserving many benefits of a declarative grammar.

LanguageLibraryParsing TechniqueNotable Features
PythonANTLR4 (via antlr4-python3-runtime)LL(*) with adaptive parsingVisitor/listener patterns, excellent error reporting, Unicode support.
PythonLarkEarley & LALR(1)Grammar written in EBNF‑like syntax, automatic tree building, supports ambiguous grammars.
PythonPLY (Python Lex‑Yacc)LALR(1)Mimics traditional Yacc/Lex, good for compilers, mature.
JavaANTLR 4LL(*)Same as Python version, strong IDE integration.
JavaScriptnearleyEarleyIncremental parsing, useful for web‑based configuration editors.
Gogo‑yacc/yaccLALR(1)Generates Go code, integrates with go build.
RustlalrpopLALR(1)Zero‑cost abstractions, integrates with Cargo.

Advantages & Disadvantages

AdvantagesDisadvantages
Reduced boilerplate – the generator handles tokenization, state machines, and parse‑tree construction.Generator learning curve – developers must learn the library’s grammar syntax and how to attach actions.
Consistent error handling – most generators produce detailed error messages (line/column, expected tokens).Runtime overhead – generated parsers may be slightly slower than hand‑tuned recursive descent for simple grammars.
Easy grammar evolution – updating the grammar file and regenerating the parser is often faster than editing hand‑written code.Ambiguity challenges – ambiguous grammars can cause exponential parse times or require disambiguation directives.
Language‑agnostic grammars – the same EBNF file can target multiple languages (if the generator supports them).Dependency on generator version – breaking changes in the generator may require grammar updates.
Integration with IDEs – syntax highlighting and grammar validation plugins exist for many editors.Limited low‑level control – fine‑grained performance tweaks (e.g., custom token buffers) are harder to achieve.

Example: Lark Grammar & Transformer

onboarding.lark

?start: device+

device: "device" NAME "{" interface* "}"
interface: "interface" NAME "{" ip_addr? "}"
ip_addr: "ip" ADDRESS "/" PREFIX_LEN

NAME: /[a-zA-Z0-9_]+/
ADDRESS: /(\d{1,3}\.){3}\d{1,3}/
PREFIX_LEN: /[0-9]+|[1-2][0-9]|3[0-2]/
%import common.WS
%ignore WS

Python transformer

from lark import Lark, Transformer

class OnboardingTransformer(Transformer):
    def device(self, items):
        name, *ifaces = items
        return {"device": str(name), "interfaces": [i for i in ifaces if isinstance(i, dict)]}
    def interface(self, items):
        name, *rest = items
        ip = rest[0] if rest and isinstance(rest[0], dict) else None
        return {"interface": str(name), "ip": ip}
    def ip_addr(self, items):
        addr, prefix = items
        return {"address": str(addr), "prefix": int(prefix)}
    def NAME(self, token): return str(token)
    def ADDRESS(self, token): return str(token)
    def PREFIX_LEN(self, token): return int(token)

def parse_onboarding(text: str):
    parser = Lark.open("onboarding.lark", parser="lalr", transformer=OnboardingTransformer())
    return parser.parse(text)

# Sample usage
sample = """
device router1 {
    interface eth0 {
        ip 10.0.0.1/24
    }
    interface eth1 {
    }
}
"""
result = parse_onboarding(sample)
print(result)

CLI Workflows

ANTLR (Java) – generate a Python parser and test it

# 1. Install ANTLR tool (requires Java)
wget https://www.antlr.org/download/antlr-4.13.1-complete.jar -O antlr.jar

# 2. Define grammar in Onboarding.g4
cat > Onboarding.g4 <<'EOF'
grammar Onboarding;
start: device+ ;
device: 'device' NAME '{' interface* '}' ;
interface: 'interface' NAME '{' ip_addr? '}' ;
ip_addr: 'ip' ADDRESS '/' PREFIX_LEN ;
NAME: [a-zA-Z0-9_]+ ;
ADDRESS: ('0'..'9')+ '.' ('0'..'9')+ '.' ('0'..'9')+ '.' ('0'..'9')+ ;
PREFIX_LEN: [0-9]+ ;
WS: [ \t\r\n]+ -> skip ;
EOF

# 3. Generate Python parser & listener
java -jar antlr.jar -Dlanguage=Python3 Onboarding.g4

# 4. Simple test driver (test_antlr.py)
cat > test_antlr.py <<'EOF'
from antlr4 import *
from OnboardingLexer import OnboardingLexer
from OnboardingParser import OnboardingParser
from OnboardingListener import OnboardingListener
import sys

class PrintListener(OnboardingListener):
    def enterDevice(self, ctx):
        print(f"Device: {ctx.NAME().getText()}")
    def enterInterface(self, ctx):
        print(f"  Interface: {ctx.NAME().getText()}")
    def enterIp_addr(self, ctx):
        print(f"    IP: {ctx.ADDRESS().getText()}/{ctx.PREFIX_LEN().getText()}")

def main():
    data = FileStream(sys.argv[1])
    lexer = OnboardingLexer(data)
    stream = CommonTokenStream(lexer)
    parser = OnboardingParser(stream)
    walker = ParseTreeWalker()
    walker.walk(PrintListener(), parser.start())

if __name__ == '__main__':
    main()
EOF

# 5. Run
python3 test_antlr.py sample.cfg

Lark – CLI utility

# Assuming lark is installed: pip install lark
lark -g onboarding.lark -e "pretty" sample.cfg

The command instantly reports parse errors (e.g., missing brace) with line/column numbers, facilitating rapid iteration.

Troubleshooting Common Issues

SymptomLikely CauseDiagnostic StepsFix
ParseError: unexpected tokenToken regex mismatch or missing %ignore WS.Run the lexer in isolation (lexer.lex(input)) to see token stream.Adjust regex, ensure whitespace is ignored, or add explicit token for the offending character.
Exponential parsing timeAmbiguous grammar causing Earley algorithm to explore many parses.Enable Lark’s ambiguity='forest' or set parser='lalr' to force deterministic parsing.Refactor grammar to eliminate left‑recursion or overlapping alternatives; add precedence directives (%left, %right).
Generated parser crashes on large inputStack overflow in recursive‑descent (Python recursion limit).Check recursion depth (sys.getrecursionlimit()).Increase limit (sys.setrecursionlimit(10000)) or switch to an iterative parser (LALR) if supported.
Semantic actions receive wrong typesTransformer methods return unexpected objects (e.g., raw tokens).Print intermediate results in transformer methods.Ensure each transformer method returns the desired Python type; use v_transformer for automatic propagation.
Generated code fails to importMissing __init__.py in generated package or version mismatch.Verify that the output directory is a Python package; check antlr version vs runtime.Add empty __init__.py, regenerate with matching runtime, or pin library versions in requirements.txt.

LLM‑Generated Extraction Rules

Overview

Instead of manually authoring a grammar or feeding it to a parser generator, a large language model (LLM) can be prompted to produce extraction rules directly from examples of the target format. These rules often take the form of regular expressions, simple parsing functions (e.g., Python snippets using re or pyparsing), or a provisional grammar that the LLM believes captures the structure. The LLM’s output is then used—often after light post‑processing—to extract fields from new configuration blobs during platform onboarding.

How LLMs Generate Extraction Rules

  1. Prompt Construction – Supply a few labeled examples (e.g., raw CLI output with highlighted fields) and ask the model: “Write a Python function that extracts device_name, interface, and ip_address from the text below.”
  2. Chain‑of‑Thought (CoT) Reasoning – The model may first explain observed patterns (delimiters, keyword positions) before emitting code.
  3. Output Format – Returns a code block (commonly fenced with triple backticks) containing the extraction logic; optionally includes a short rationale.
  4. Post‑Processing – A harness strips the fencing, validates syntax (e.g., python -m py_compile), and optionally runs unit tests on the supplied examples.
  5. Iterative Refinement – If the initial rule fails on additional samples, feed the failures back to the model for correction.

Advantages & Disadvantages

AdvantagesDisadvantages
Rapid prototyping – A usable extractor can be produced in minutes from a handful of examples, bypassing grammar authoring.Non‑deterministic output – Same prompt may yield different code across runs, complicating version control.
Handles irregular formats – LLMs excel at inferring patterns from noisy, semi‑structured text (e.g., logs with optional fields).Limited guarantees – No formal proof that the generated regex covers all edge cases; may over‑match or under‑match.
Low barrier to entry – No need to learn EBNF or parser‑generator specifics.Security & sandboxing concerns – Arbitrary code generated by an LLM must be executed in a restricted environment.
Can produce hybrid solutions – Model may output a combination of regex + lightweight parsing logic that is easier to maintain than a full grammar.Difficulty scaling to complex grammars – For deeply nested or recursive structures, LLMs often produce brittle regexes that fail on edge cases.
Documentation generation – The model can simultaneously emit comments explaining the regex, aiding future maintenance.Prompt sensitivity – Small changes in wording or example ordering can dramatically affect the quality of the output.

Example Use Cases

Sample LLM‑Generated Extractor (Python)

import re

def extract_interface_info(text: str):
    """
    Extracts interface name and IP address from lines like:
        interface GigabitEthernet0/1
          ip address 10.0.0.1 255.255.255.0
    Returns a list of dicts: [{'interface': 'GigabitEthernet0/1', 'ip': '10.0.0.1', 'mask': '255.255.255.0'}, ...]
    """
    pattern = re.compile(
        r"interface\s+(?P<iface>\S+)\s*\n\s*ip\s+address\s+(?P<ip>\d+\.\d+\.\d+\.\d+)\s+(?P<mask>\d+\.\d+\.\d+\.\d+)",
        re.IGNORECASE | re.MULTILINE,
    )
    return [m.groupdict() for m in pattern.finditer(text)]

# Example usage
sample = """
interface GigabitEthernet0/1
  ip address 10.0.0.1 255.255.255.0
interface GigabitEthernet0/2
  no ip address
"""
print(extract_interface_info(sample))

Verification Harness for Production‑Ready Output

Before any parsed or extracted data reaches production workflows, it must pass a rigorous verification harness. The harness combines static checks, dynamic testing, and safety guarantees to ensure correctness, security, and performance.

1. Static Analysis

2. Unit‑Test Suite


Share this post on:

Previous Post
Why readiness goes green while forwarding stays dead
Next Post
Breakouts, subinterfaces, and LAGs as first-class data