← All modules

Distribution · data formats

DFM::Parser

Divergent

A grammar for Delphi form files — no actions, no serialiser, and a number list of more than one number never parses.

Version
0.0.1 zef:massa
Depends
none beyond the core
License
Artistic-2.0
Its own test suite
1 file, green
Checked
2026-09-15 against Raku++ 3.28.0 and Rakudo 2026.08
Where it lives
raku.land · source

Install it #

$ rakupp install DFM::Parser

zef install DFM::Parser writes the same store; either installer leaves the module usable by both engines.

What it is for #

A .dfm file is the textual form of a Delphi form: nested object … end blocks with Name = Value components. Reading one — to migrate a UI, to audit a legacy application, to extract strings — needs a parser for its small, idiosyncratic syntax.

This distribution is the grammar. It ships no actions class and no serialiser, so you get a raw Match tree and do your own walking.

Parsing #

File
use DFM::Parser;

my $dfm = q:to/END/;
object Form1: TForm1
  Left = 192
  Caption = 'Hello'
  Font.Height = -11
  object Panel1: TPanel
    Align = alTop
  end
end
END

my $m = DFM::Parser.parse($dfm);
say 'parsed      : ', $m.defined;
say 'id          : ', $m<object><id>.Str;
say 'classname   : ', $m<object><classname>.Str;
say '';
for $m<object><component>.list -> $c {
    if $c<object> {
        say '  nested object: ', $c<object><classname>.Str;
    } else {
        say sprintf('  %-14s = %s', $c<component-name>.Str, $c<component-value>.Str.trim);
    }
}
Output
parsed      : True
id          : Form1
classname   : TForm1

  Left           = 192
  Caption        = 'Hello'
  Font.Height    = -11
  nested object: TPanel

The one thing to know #

A number list with more than one number never parses.

File
use DFM::Parser;

for '(1 2)', '(1)', '()', "('a' 'b')" -> $v {
    my $m = DFM::Parser.parse($v, rule => 'component-value');
    say sprintf('  component-value %-10s -> %s', $v.raku, $m.defined ?? 'MATCH' !! 'no match');
}
say '';
say 'number-list is a `rule` whose body is <number>*, and `number` is a';
say '`token` — so nothing eats the whitespace BETWEEN repetitions. A';
say 'rule`s implicit <.ws> sits after the quantified atom, not inside it.';
say '';
say 'string-list survives only because `string` is itself a rule and';
say 'swallows its own trailing space.';
say '';
say 'any real .dfm containing a Left = (0 0 100 100) fails outright, and';
say 'you get Nil, not a diagnostic.';
Output
  component-value "(1 2)"    -> no match
  component-value "(1)"      -> MATCH
  component-value "()"       -> MATCH
  component-value "('a' 'b')" -> MATCH

number-list is a `rule` whose body is <number>*, and `number` is a
`token` — so nothing eats the whitespace BETWEEN repetitions. A
rule`s implicit <.ws> sits after the quantified atom, not inside it.

string-list survives only because `string` is itself a rule and
swallows its own trailing space.

any real .dfm containing a Left = (0 0 100 100) fails outright, and
you get Nil, not a diagnostic.

Every failure is Nil #

File
use DFM::Parser;

my %cases =
    'well formed'      => "object A: TA\n  X = 1\nend\n",
    'missing end'      => "object A: TA\n  X = 1\n",
    'total garbage'    => "not a form at all\n",
    'empty string'     => '',
    'unterminated str' => "object A: TA\n  X = 'oops\nend\n";

for %cases.keys.sort -> $k {
    my $m = DFM::Parser.parse(%cases{$k});
    say sprintf('  %-18s -> defined=%-6s isFailure=%s',
                $k, $m.defined, ($m ~~ Failure).so);
}
say '';
say 'never an exception, never a Failure — always Nil. And the failure is';
say 'easy to miss, because indexing the result gives silent garbage:';
my $bad = DFM::Parser.parse('garbage');
say '  $bad<object>          : ', $bad<object>.raku;
say '  $bad<o><c>.elems      : ', $bad<o><c>.elems, '   <- Any.elems is 1';
say '';
say 'test .defined on the result; never trust .elems of a chained subscript.';
Output
  empty string       -> defined=False  isFailure=False
  missing end        -> defined=False  isFailure=False
  total garbage      -> defined=False  isFailure=False
  unterminated str   -> defined=False  isFailure=False
  well formed        -> defined=True   isFailure=False

never an exception, never a Failure — always Nil. And the failure is
easy to miss, because indexing the result gives silent garbage:
  $bad<object>          : Any
  $bad<o><c>.elems      : 1   <- Any.elems is 1

test .defined on the result; never trust .elems of a chained subscript.

What the grammar does not check #

File
use DFM::Parser;

my %cases =
    'spaced slashes'     => "object A / TA\nend\n",
    'nine on rank one'   => "object A: TA\n  X = 1\nend\n",
    'seven ranks only'   => "object A: TA\nend\n",
    'dotted name'        => "object A: TA\n  Font.Height = -11\nend\n",
    'hashtag char'       => "object A: TA\n  S = #65#66\nend\n";
for %cases.keys.sort -> $k {
    say sprintf('  %-20s -> %s', $k,
                DFM::Parser.parse(%cases{$k}).defined ?? 'parses' !! 'no parse');
}
say '';
say 'TOP accepts exactly ONE object and is not anchored for leading';
say 'comments, so a real file with a header or two top-level objects will';
say 'not parse either.';
Output
  dotted name          -> parses
  hashtag char         -> parses
  nine on rank one     -> parses
  seven ranks only     -> parses
  spaced slashes       -> no parse

TOP accepts exactly ONE object and is not anchored for leading
comments, so a real file with a header or two top-level objects will
not parse either.

Where the two engines differ #

Bracketed identifier sets — BorderIcons = [biSystemMenu, biMinimize], which almost every real form has — are unreachable through component-value under Raku++, although :rule<dset> matches them directly there.

File
use DFM::Parser;

say 'the rule matches on its own:';
say '  :rule<dset> on "[a, b]" -> ',
    DFM::Parser.parse('[a, b]', rule => 'dset').defined ?? 'MATCH' !! 'no match';
say '';
say 'reaching it through component-value is engine-dependent — Raku++`s';
say 'longest-token alternation cannot select a branch containing a';
say '%-separated quantifier, so a form with a BorderIcons line parses on';
say 'Rakudo and fails on Raku++.';
say '';
say 'and the rule will not accept internal spaces on either engine:';
for '[a,b]', '[a, b]', '[ a , b ]' -> $v {
    say sprintf('  %-12s -> %s', $v.raku,
                DFM::Parser.parse($v, rule => 'dset').defined ?? 'MATCH' !! 'no match');
}
Output
the rule matches on its own:
  :rule<dset> on "[a, b]" -> MATCH

reaching it through component-value is engine-dependent — Raku++`s
longest-token alternation cannot select a branch containing a
%-separated quantifier, so a form with a BorderIcons line parses on
Rakudo and fails on Raku++.

and the rule will not accept internal spaces on either engine:
  "[a,b]"      -> MATCH
  "[a, b]"     -> MATCH
  "[ a , b ]"  -> no match