From c2bdb71d03f38d3b3f603b439e38c383895992ed Mon Sep 17 00:00:00 2001 From: Jonathan Worthington Date: Tue, 6 Jan 2026 17:54:12 +0100 Subject: [PATCH] Implement `for` loops and array literals For loops mostly use the Raku syntax (`->` style), however since our `if` and `given` all currently require parens around the condition or topic, we require them here too for consistency. We allow multiple loop variables for iterating two items at a time. For hashes, we do a key/value iteration, so you iterate the two at once and put them into seperate variables. This isn't quite Perl or Raku, but it is the path of least resistance from what we have today to getting something useful. (We also don't go down the path of sigils beyond the existing `$`.) Finally, array literals like `[1, 2]` are also introduced. They can be used directly with `for` or assigned to a variable. There are no other operations on arrays for now; they are introduced primarily in support of `for`. --- lib/Agrammon/Environment.rakumod | 4 + lib/Agrammon/Formula.rakumod | 60 ++++++++++--- lib/Agrammon/Formula/Builder.rakumod | 13 +++ lib/Agrammon/Formula/Compiler.rakumod | 10 +++ lib/Agrammon/Formula/Parser.rakumod | 13 +++ t/formula.rakutest | 120 ++++++++++++++++++++++++++ 6 files changed, 207 insertions(+), 13 deletions(-) diff --git a/lib/Agrammon/Environment.rakumod b/lib/Agrammon/Environment.rakumod index e8d735a66..80c9e4ae0 100644 --- a/lib/Agrammon/Environment.rakumod +++ b/lib/Agrammon/Environment.rakumod @@ -20,4 +20,8 @@ class Agrammon::Environment { method find-builtin($name) { %!builtins{$name} // get-builtins(){$name} // die "No such builtin function '$name'"; } + + method iterate($value) { + $value ~~ Map ?? $value.kv !! $value.list + } } diff --git a/lib/Agrammon/Formula.rakumod b/lib/Agrammon/Formula.rakumod index 2a0aa376a..6e9aebf0b 100644 --- a/lib/Agrammon/Formula.rakumod +++ b/lib/Agrammon/Formula.rakumod @@ -41,6 +41,14 @@ class Agrammon::Formula::StatementList does Agrammon::Formula { } } +class Agrammon::Formula::Block does Agrammon::Formula { + has Agrammon::Formula::StatementList $.statements; + + method input-used() { $!statements.input-used } + method technical-used() { $!statements.technical-used } + method output-used() { $!statements.output-used } +} + class Agrammon::Formula::Routine does Agrammon::Formula { has Agrammon::Formula::StatementList $.statements; @@ -49,6 +57,14 @@ class Agrammon::Formula::Routine does Agrammon::Formula { method output-used() { $!statements.output-used } } +class Agrammon::Formula::VarDecl does Agrammon::Formula::LValue { + has Str $.name; +} + +class Agrammon::Formula::Var does Agrammon::Formula::LValue { + has Str $.name; +} + class Agrammon::Formula::If does Agrammon::Formula { has Agrammon::Formula $.condition; has Agrammon::Formula $.then; @@ -70,12 +86,22 @@ class Agrammon::Formula::If does Agrammon::Formula { } } -class Agrammon::Formula::Block does Agrammon::Formula { - has Agrammon::Formula::StatementList $.statements; +class Agrammon::Formula::For does Agrammon::Formula { + has Agrammon::Formula $.iterable; + has Agrammon::Formula::VarDecl @.loop-vars; + has Agrammon::Formula::Block $.block; - method input-used() { $!statements.input-used } - method technical-used() { $!statements.technical-used } - method output-used() { $!statements.output-used } + method input-used() { + self!merge-inputs: $!iterable.input-used, $!block.input-used + } + + method technical-used() { + self!merge-technicals: $!iterable.technical-used, $!block.technical-used + } + + method output-used() { + self!merge-outputs: $!iterable.output-used, $!block.output-used + } } class Agrammon::Formula::Given does Agrammon::Formula { @@ -145,14 +171,6 @@ class Agrammon::Formula::WhenMod does Agrammon::Formula { } } -class Agrammon::Formula::VarDecl does Agrammon::Formula::LValue { - has Str $.name; -} - -class Agrammon::Formula::Var does Agrammon::Formula::LValue { - has Str $.name; -} - class Agrammon::Formula::CallBuiltin does Agrammon::Formula::LValue { has Str $.name; has Agrammon::Formula @.args; @@ -217,6 +235,22 @@ class Agrammon::Formula::Sum does Agrammon::Formula { method output-used() { ($!reference,) } } +class Agrammon::Formula::Array does Agrammon::Formula { + has Agrammon::Formula @.values; + + method input-used() { + self!merge-inputs: @!values.map(*.input-used) + } + + method technical-used() { + self!merge-technicals: @!values.map(*.technical-used) + } + + method output-used() { + self!merge-outputs: @!values.map(*.output-used) + } +} + class Agrammon::Formula::Hash does Agrammon::Formula { has Agrammon::Formula @.pairs; diff --git a/lib/Agrammon/Formula/Builder.rakumod b/lib/Agrammon/Formula/Builder.rakumod index 712394491..eefea474c 100644 --- a/lib/Agrammon/Formula/Builder.rakumod +++ b/lib/Agrammon/Formula/Builder.rakumod @@ -97,6 +97,13 @@ class Agrammon::Formula::Builder { ) } + method statement_control:sym($/) { + make Agrammon::Formula::For.new( + iterable => $.ast, + loop-vars => $.map(-> $var { Agrammon::Formula::VarDecl.new(name => ~$var) }), + block => $.ast + ); + } method block($/) { make Agrammon::Formula::Block.new( @@ -262,6 +269,12 @@ class Agrammon::Formula::Builder { make $.ast; } + method term:sym<[ ]>($/) { + make Agrammon::Formula::Array.new( + values => $.map(*.ast) + ); + } + method term:sym<{ }>($/) { make Agrammon::Formula::Hash.new( pairs => $.map(*.ast) diff --git a/lib/Agrammon/Formula/Compiler.rakumod b/lib/Agrammon/Formula/Compiler.rakumod index d3589575e..b58cb01f6 100644 --- a/lib/Agrammon/Formula/Compiler.rakumod +++ b/lib/Agrammon/Formula/Compiler.rakumod @@ -41,6 +41,12 @@ multi compile(Agrammon::Formula::Default $default) { q:f"default { &compile($default.block) }" } +multi compile(Agrammon::Formula::For $for) { + q:f"for $env.iterate(&compile($for.iterable)) -> " ~ + $for.loop-vars.map(*.name).join(", ") ~ + q:f" { &compile($for.block) }" +} + multi compile(Agrammon::Formula::WhenMod $when) { q:f"&compile($when.then) when &compile($when.test)" } @@ -69,6 +75,10 @@ multi compile(Agrammon::Formula::Sum $sum) { } } +multi compile(Agrammon::Formula::Array $array) { + q:c"@( {$array.values.map(&compile).join(',')} )" +} + multi compile(Agrammon::Formula::Hash $hash) { q:c"%( {$hash.pairs.map(&compile).join(',')} )" } diff --git a/lib/Agrammon/Formula/Parser.rakumod b/lib/Agrammon/Formula/Parser.rakumod index 22bac8b52..3d0455961 100644 --- a/lib/Agrammon/Formula/Parser.rakumod +++ b/lib/Agrammon/Formula/Parser.rakumod @@ -64,6 +64,13 @@ grammar Agrammon::Formula::Parser { 'default' } + rule statement_control:sym { + 'for' + [ '(' ')' || <.panic('Missing or malformed loop expression')> ] + ['->' + % [ ',' ] || <.panic('Missing or malformed loop variables')> ] + + } + rule block { [ '{' || <.panic('Expected block')> ] @@ -152,6 +159,12 @@ grammar Agrammon::Formula::Parser { '(' [ ')' || <.panic('Missing closing )')> ] } + rule term:sym<[ ]> { + '[' + * %% [ ',' ] + [ ']' || <.panic('Missing ] on array literal or malformed array')> ] + } + rule term:sym<{ }> { '{' * %% [ ',' ] diff --git a/t/formula.rakutest b/t/formula.rakutest index c4b21c6da..ffc77ed61 100644 --- a/t/formula.rakutest +++ b/t/formula.rakutest @@ -1098,4 +1098,124 @@ subtest { is $result.Numeric, 42, 'Correct result'; }, 'P+ upgrades scalar value on left side'; +subtest { + my $f = parse-formula(q:to/FORMULA/, 'TestModule'); + my $total = 0; + for ([1, 2, 3]) -> $num { + $total = $total + $num; + } + return $total; + FORMULA + ok $f ~~ Agrammon::Formula, 'Get something doing Agrammon::Formula from parse'; + is-deeply $f.input-used, (), 'Correct inputs-used'; + is-deeply $f.technical-used, (), 'Correct technical-used'; + is-deeply $f.output-used, (), 'Correct output-used'; + my $result = compile-and-evaluate($f, Agrammon::Environment.new); + is $result, 6, 'Correct result from for loop over array literal'; +}, 'Basic for loop over array literal'; + +subtest { + my $f = parse-formula(q:to/FORMULA/, 'TestModule'); + my $nums = [1, 2, 3]; + my $total = 0; + for ($nums) -> $num { + $total = $total + $num; + } + return $total; + FORMULA + ok $f ~~ Agrammon::Formula, 'Get something doing Agrammon::Formula from parse'; + is-deeply $f.input-used, (), 'Correct inputs-used'; + is-deeply $f.technical-used, (), 'Correct technical-used'; + is-deeply $f.output-used, (), 'Correct output-used'; + my $result = compile-and-evaluate($f, Agrammon::Environment.new); + is $result, 6, 'Correct result from for loop over variable'; +}, 'For loop over variable containing array'; + +subtest { + my $f = parse-formula(q:to/FORMULA/, 'TestModule'); + my $total = 0; + for ([3, 5, 7, 9]) -> $a, $b { + $total = $total + $a * $b; + } + return $total; + FORMULA + ok $f ~~ Agrammon::Formula, 'Get something doing Agrammon::Formula from parse'; + is-deeply $f.input-used, (), 'Correct inputs-used'; + is-deeply $f.technical-used, (), 'Correct technical-used'; + is-deeply $f.output-used, (), 'Correct output-used'; + my $result = compile-and-evaluate($f, Agrammon::Environment.new); + is $result, 3*5 + 7*9, 'Correct result from for loop taking two values at a time'; +}, 'For loop taking two values at a time'; + +subtest { + my $f = parse-formula(q:to/FORMULA/, 'TestModule'); + my $total = 0; + for ({ a => 1, b => 3 }) -> $key, $value { + $total = $total + $value; + } + return $total; + FORMULA + ok $f ~~ Agrammon::Formula, 'Get something doing Agrammon::Formula from parse'; + is-deeply $f.input-used, (), 'Correct inputs-used'; + is-deeply $f.technical-used, (), 'Correct technical-used'; + is-deeply $f.output-used, (), 'Correct output-used'; + my $result = compile-and-evaluate($f, Agrammon::Environment.new); + is $result, 4, 'Correct result from for loop over hash literal'; +}, 'For loop over hash literal'; + +subtest { + my $f = parse-formula(q:to/FORMULA/, 'TestModule'); + my $mapping = { x => 10, y => 20 }; + my $total = 0; + for ($mapping) -> $key, $value { + $total = $total + $value; + } + return $total; + FORMULA + ok $f ~~ Agrammon::Formula, 'Get something doing Agrammon::Formula from parse'; + is-deeply $f.input-used, (), 'Correct inputs-used'; + is-deeply $f.technical-used, (), 'Correct technical-used'; + is-deeply $f.output-used, (), 'Correct output-used'; + my $result = compile-and-evaluate($f, Agrammon::Environment.new); + is $result, 30, 'Correct result from for loop over variable containing hash'; +}, 'For loop over variable containing hash'; + +subtest { + my $f = parse-formula(q:to/FORMULA/, 'TestModule'); + my $h = { apple => 2, banana => 3 }; + my $keys = ''; + my $total = 0; + for ($h) -> $key, $value { + $keys = $keys . $key . ','; + $total = $total + $value; + } + return $keys . $total; + FORMULA + ok $f ~~ Agrammon::Formula, 'Get something doing Agrammon::Formula from parse'; + is-deeply $f.input-used, (), 'Correct inputs-used'; + is-deeply $f.technical-used, (), 'Correct technical-used'; + is-deeply $f.output-used, (), 'Correct output-used'; + my $result = compile-and-evaluate($f, Agrammon::Environment.new); + ok $result eq 'apple,banana,5' || $result eq 'banana,apple,5', + 'Correct result with keys concatenated (either order)'; +}, 'For loop over hash verifying keys'; + +subtest { + my $f = parse-formula(q:to/FORMULA/, 'TestModule'); + my $res = 0; + for (['bc', 'th', 'ts']) -> $tech { + $res = $res + $TE->{'app_tech_' . $tech}; + } + return $res; + FORMULA + ok $f ~~ Agrammon::Formula, 'Get something doing Agrammon::Formula from parse'; + is-deeply $f.input-used, (), 'Correct inputs-used'; + is-deeply $f.technical-used, (), 'Correct technical-used (indirect not included)'; + is-deeply $f.output-used, (), 'Correct output-used'; + my $result = compile-and-evaluate($f, Agrammon::Environment.new( + technical => { app_tech_bc => 10, app_tech_th => 20, app_tech_ts => 30 } + )); + is $result, 60, 'Correct result from for loop with dynamic technical access'; +}, 'For loop with indirect technical access'; + done-testing;