diff --git a/lib/_007/Parser/Actions.pm6 b/lib/_007/Parser/Actions.pm6 index 14500e51..a0e73381 100644 --- a/lib/_007/Parser/Actions.pm6 +++ b/lib/_007/Parser/Actions.pm6 @@ -159,7 +159,7 @@ class _007::Parser::Actions { die "Unknown routine type $"; # XXX: Turn this into an X:: exception } - $identifier.put-value($val, $*runtime); + $*runtime.put-var($identifier, $val); $*parser.opscope.maybe-install($name, $); } @@ -205,7 +205,7 @@ class _007::Parser::Actions { method attributes \{ () \} method ^name(\$) \{ "{$identifier.name.value}" \} \}]); - $identifier.put-value($val, $*runtime); + $*runtime.put-var($identifier, $val); } method traitlist($/) { @@ -458,9 +458,8 @@ class _007::Parser::Actions { method prefix($/) { my $op = ~$/; - my $identifier = Q::Identifier.new( + my $identifier = Q::Term::Identifier.new( :name(Val::Str.new(:value("prefix:$op"))), - :frame($*runtime.current-frame), ); make $*parser.opscope.ops{$op}.new(:$identifier, :operand(Val::None)); } @@ -530,7 +529,7 @@ class _007::Parser::Actions { } method regex-fragment:identifier ($/) { - make Q::Regex::Identifier.new(:identifier($.ast)); + make Q::Regex::Identifier.new(:identifier($.ast)); } method regex-fragment:call ($/) { @@ -546,7 +545,6 @@ class _007::Parser::Actions { } method term:identifier ($/) { - make $.ast; my $name = $.ast.name.value; if !$*runtime.declared($name) { my $frame = $*runtime.current-frame; @@ -558,6 +556,7 @@ class _007::Parser::Actions { unless $value ~~ Val::Func; }; } + make Q::Term::Identifier.new(:name($.ast.name)); } method term:block ($/) { @@ -618,7 +617,7 @@ class _007::Parser::Actions { my $outer-frame = $*runtime.current-frame.properties; my $static-lexpad = $*runtime.current-frame.properties; my $val = Val::Func.new(:$name, :$parameterlist, :$statementlist, :$outer-frame, :$static-lexpad); - $.ast.put-value($val, $*runtime); + $*runtime.put-var($.ast, $val); } finish-block($block); @@ -688,11 +687,12 @@ class _007::Parser::Actions { method term:my ($/) { my $identifier = $.ast; - my $name = $identifier.name; - make Q::Term::My.new(:identifier($identifier)); + make Q::Term::My.new( + :identifier(Q::Term::Identifier.new(:name($identifier.name))) + ); - $*parser.opscope.maybe-install($name, []); + $*parser.opscope.maybe-install($identifier.name, []); } method propertylist ($/) { @@ -716,8 +716,10 @@ class _007::Parser::Actions { } method property:identifier ($/) { - my $key = $.ast.name; - make Q::Property.new(:$key, :value($.ast)); + self."term:identifier"($/); + my $value = $/.ast; + my $key = $value.name; + make Q::Property.new(:$key, :$value); } method property:method ($/) { @@ -733,7 +735,7 @@ class _007::Parser::Actions { method infix($/) { my $op = ~$/; - my $identifier = Q::Identifier.new( + my $identifier = Q::Term::Identifier.new( :name(Val::Str.new(:value("infix:$op"))), ); make $*parser.opscope.ops{$op}.new(:$identifier, :lhs(NONE), :rhs(NONE)); @@ -758,9 +760,8 @@ class _007::Parser::Actions { elsif $ { $op = "."; } - my $identifier = Q::Identifier.new( + my $identifier = Q::Term::Identifier.new( :name(Val::Str.new(:value("postfix:$op"))), - :frame($*runtime.current-frame), ); # XXX: this can't stay hardcoded forever, but we don't have the machinery yet # to do these right enough diff --git a/lib/_007/Parser/Syntax.pm6 b/lib/_007/Parser/Syntax.pm6 index c778a3f7..0d42874d 100644 --- a/lib/_007/Parser/Syntax.pm6 +++ b/lib/_007/Parser/Syntax.pm6 @@ -35,15 +35,15 @@ grammar _007::Parser::Syntax { die X::Syntax::Missing.new(:$what); } - our sub declare(Q::Declaration $decltype, $symbol) { + our sub declare(Q::Declaration $decltype, Str $symbol) { die X::Redeclaration.new(:$symbol) if $*runtime.declared-locally($symbol); my $frame = $*runtime.current-frame(); die X::Redeclaration::Outer.new(:$symbol) if %*assigned{$frame.WHICH ~ $symbol}; my $identifier = Q::Identifier.new( - :name(Val::Str.new(:value($symbol))), - :$frame); + :name(Val::Str.new(:value($symbol))) + ); $*runtime.declare-var($identifier); @*declstack[*-1]{$symbol} = $decltype; } @@ -180,10 +180,12 @@ grammar _007::Parser::Syntax { } token regex-fragment:identifier { + # XXX: should be term:identifier } token regex-fragment:call { '<' ~ '>' + # XXX: should be term:identifier } rule regex-fragment:group { '' @@ -212,7 +214,7 @@ grammar _007::Parser::Syntax { || "<" <.ws> $=["Q.Prefix"] ">" <.ws> '{' <.ws> <.ws> '}' || "<" <.ws> $=["Q.Postfix"] ">" <.ws> '{' <.ws> <.ws> '}' || "<" <.ws> $=["Q.Expr"] ">" <.ws> '{' <.ws> <.ws> '}' - || "<" <.ws> $=["Q.Identifier"] ">" <.ws> '{' <.ws> <.ws> '}' + || "<" <.ws> $=["Q.Identifier"] ">" <.ws> '{' <.ws> <.ws> '}' || "<" <.ws> $=["Q.Block"] ">" <.ws> '{' <.ws> <.ws> '}' || "<" <.ws> $=["Q.CompUnit"] ">" <.ws> '{' <.ws> [ || ] <.ws> '}' || "<" <.ws> $=["Q.Literal"] ">" <.ws> '{' <.ws> [ | | ] <.ws> '}' @@ -240,6 +242,11 @@ grammar _007::Parser::Syntax { } token term:new-object { new» <.ws> + # XXX: Deliberately introducing a bug here. Sorry! + # The first of the below identifiers should be a term:identifier, and the lookup + # should adapt to that fact. #250 would help sort this out. We're getting away + # with this because we're essentially missing a tricky-enough test, probably + # involving quasis and/or macros. + % [<.ws> "." <.ws>] .map(&prefix:<~>).map(-> $identifier { @@ -301,7 +308,9 @@ grammar _007::Parser::Syntax { :!s <.finishpad> } - token property:identifier { } + token property:identifier { + + } method infix { my @ops = $*parser.opscope.ops.keys; diff --git a/lib/_007/Q.pm6 b/lib/_007/Q.pm6 index c27cd24b..5db1beb9 100644 --- a/lib/_007/Q.pm6 +++ b/lib/_007/Q.pm6 @@ -93,6 +93,16 @@ role Q { } } +### ### Q::Identifier +### +### An identifier; a name in code. +### +class Q::Identifier does Q { + has Val::Str $.name; + + method attribute-order { } +} + ### ### Q::Expr ### ### An expression; something that can be evaluated to a value. @@ -161,25 +171,39 @@ class Q::Literal::Str does Q::Literal { method eval($) { $.value } } -### ### Q::Identifier +### ### Q::Term::Identifier ### ### An identifier; a name which identifies a storage location in the program. ### -### Identifiers are subject to *scoping*: the same name can point to different -### storage locations because they belong to different scopes. +### Identifiers in expressions are subject to *scoping*: the same name can +### point to different storage locations because they belong to different scopes. +### The same name in the same scope might even point to different storage +### locations at different times when accessed from different call frames. ### -class Q::Identifier does Q::Term { - has Val::Str $.name; - has $.frame = NONE; +class Q::Term::Identifier is Q::Identifier does Q::Term { + method eval($runtime) { + return $runtime.get-var($.name.value); + } - method attribute-order { } + method put-value($value, $runtime) { + $runtime.put-var(self, $value); + } +} + +### ### Q::Term::Identifier::Direct +### +### A direct identifier; a name which directly identifies a storage location +### in the program. +### +class Q::Term::Identifier::Direct is Q::Term::Identifier { + has Val::Dict $.frame; method eval($runtime) { - return $runtime.get-var($.name.value, $.frame); + return $runtime.get-direct($.frame, $.name.value); } method put-value($value, $runtime) { - $runtime.put-var(self, $value); + $runtime.put-direct($.frame, $.name.value, $value); } } @@ -405,8 +429,6 @@ class Q::Block does Q { has $.parameterlist; has $.statementlist; has Val::Dict $.static-lexpad is rw = Val::Dict.new; - # XXX - has $.frame is rw; method attribute-order { } } @@ -663,6 +685,26 @@ class Q::Unquote::Infix is Q::Unquote { has $.rhs; } +### ### Q::Term::My +### +### A `my` variable declaration. +### +class Q::Term::My does Q::Term does Q::Declaration { + has $.identifier; + + method is-assignable { True } + + method eval($runtime) { + return $.identifier.eval($runtime); + } + + method put-value($value, $runtime) { + $.identifier.put-value($value, $runtime); + } +} + +class Q::StatementList { ... } + ### ### Q::Term::Quasi ### ### A quasi; a piece of 007 code which evaluates to that code's Qtree @@ -681,7 +723,7 @@ class Q::Term::Quasi does Q::Term { method attribute-order { } method eval($runtime) { - my $needs-displacement = $.contents !~~ Q::Block; + my $quasi-frame; sub interpolate($thing) { return $thing.new(:elements($thing.elements.map(&interpolate))) @@ -693,7 +735,16 @@ class Q::Term::Quasi does Q::Term { return $thing if $thing ~~ Val; - return $thing.new(:name($thing.name), :frame($needs-displacement ?? $runtime.current-frame !! NONE)) + if $thing ~~ Q::Term::Identifier { + if $runtime.lookup-frame-outside($thing, $quasi-frame) -> $frame { + return Q::Term::Identifier::Direct.new(:name($thing.name), :$frame); + } + else { + return $thing; + } + } + + return $thing.new(:name($thing.name)) if $thing ~~ Q::Identifier; if $thing ~~ Q::Unquote::Prefix { @@ -716,20 +767,39 @@ class Q::Term::Quasi does Q::Term { return $ast; } + if $thing ~~ Q::Term::My { + $runtime.declare-var($thing.identifier); + } + + if $thing ~~ Q::Term::Func { + $runtime.enter($runtime.current-frame, Val::Dict.new, Q::StatementList.new); + for $thing.block.parameterlist.parameters.elements.map(*.identifier) -> $identifier { + $runtime.declare-var($identifier); + } + } + + if $thing ~~ Q::Block { + $runtime.enter($runtime.current-frame, Val::Dict.new, $thing.statementlist); + } + my %attributes = $thing.attributes.map: -> $attr { aname($attr) => interpolate(avalue($attr, $thing)) }; + if $thing ~~ Q::Term::Func || $thing ~~ Q::Block { + $runtime.leave(); + } + $thing.new(|%attributes); } if $.qtype.value eq "Q.Unquote" && $.contents ~~ Q::Unquote { return $.contents; } + $runtime.enter($runtime.current-frame, Val::Dict.new, Q::StatementList.new); + $quasi-frame = $runtime.current-frame; my $r = interpolate($.contents); - if $r ~~ Q::Block { - $r.frame = $runtime.current-frame; - } + $runtime.leave(); return $r; } } @@ -768,24 +838,6 @@ class Q::ArgumentList does Q { role Q::Statement does Q { } -### ### Q::Term::My -### -### A `my` variable declaration. -### -class Q::Term::My does Q::Term does Q::Declaration { - has $.identifier; - - method is-assignable { True } - - method eval($runtime) { - return $.identifier.eval($runtime); - } - - method put-value($value, $runtime) { - $.identifier.put-value($value, $runtime); - } -} - ### ### Q::Statement::Expr ### ### A statement consisting of an expression. @@ -1034,7 +1086,7 @@ class Q::Expr::BlockAdapter does Q::Expr { has $.block; method eval($runtime) { - $runtime.enter($.block.frame, $.block.static-lexpad, $.block.statementlist); + $runtime.enter($runtime.current-frame, $.block.static-lexpad, $.block.statementlist); my $result = $.block.statementlist.run($runtime); $runtime.leave; return $result; diff --git a/lib/_007/Runtime.pm6 b/lib/_007/Runtime.pm6 index 5ec41fd1..149085a7 100644 --- a/lib/_007/Runtime.pm6 +++ b/lib/_007/Runtime.pm6 @@ -4,9 +4,7 @@ use _007::Builtins; use _007::Equal; constant NO_OUTER = Val::Dict.new; -constant RETURN_TO = Q::Identifier.new( - :name(Val::Str.new(:value("--RETURN-TO--"))), - :frame(NONE)); +constant RETURN_TO = Q::Identifier.new(:name(Val::Str.new(:value("--RETURN-TO--")))); constant EXIT_SUCCESS = 0; class _007::Runtime { @@ -79,9 +77,7 @@ class _007::Runtime { my $frame = Val::Dict.new(:properties(:$outer-frame, :pad(Val::Dict.new))); @!frames.push($frame); for $static-lexpad.properties.kv -> $name, $value { - my $identifier = Q::Identifier.new( - :name(Val::Str.new(:value($name))), - :frame(NONE)); + my $identifier = Q::Identifier.new(:name(Val::Str.new(:value($name)))); self.declare-var($identifier, $value); } for $statementlist.statements.elements.kv -> $i, $_ { @@ -103,7 +99,7 @@ class _007::Runtime { } if $routine { my $name = $routine.name; - my $identifier = Q::Identifier.new(:$name, :$frame); + my $identifier = Q::Identifier.new(:$name); self.declare-var($identifier, $routine); } } @@ -140,17 +136,30 @@ class _007::Runtime { if $symbol eq RETURN_TO; } + method lookup-frame-outside(Q::Term::Identifier $identifier, $quasi-frame) { + my Str $name = $identifier.name.value; + my $frame = self.current-frame; + my $seen-quasi-frame = False; + repeat until $frame === NO_OUTER { + if $frame.properties.properties{$name} :exists { + return $seen-quasi-frame ?? $frame !! Nil; + } + if $frame === $quasi-frame { + $seen-quasi-frame = True; + } + $frame = $frame.properties; + } + die "something is very off with lexical lookup ($name)"; # XXX: turn into X:: + } + method put-var(Q::Identifier $identifier, $value) { my $name = $identifier.name.value; - my $frame = $identifier.frame ~~ Val::None - ?? self.current-frame - !! $identifier.frame; - my $pad = self!find-pad($name, $frame); + my $pad = self!find-pad($name, self.current-frame); $pad.properties{$name} = $value; } - method get-var(Str $name, $frame = self.current-frame) { - my $pad = self!find-pad($name, $frame); + method get-var(Str $name) { + my $pad = self!find-pad($name, self.current-frame); return $pad.properties{$name}; } @@ -160,12 +169,17 @@ class _007::Runtime { } } + method get-direct(Val::Dict $frame, Str $name) { + return $frame.properties.properties{$name}; + } + + method put-direct(Val::Dict $frame, Str $name, $value) { + $frame.properties.properties{$name} = $value; + } + method declare-var(Q::Identifier $identifier, $value?) { my $name = $identifier.name.value; - my Val::Dict $frame = $identifier.frame ~~ Val::None - ?? self.current-frame - !! $identifier.frame; - $frame.properties.properties{$name} = $value // NONE; + self.current-frame.properties.properties{$name} = $value // NONE; } method declared($name) { @@ -173,9 +187,7 @@ class _007::Runtime { } method declared-locally($name) { - my $frame = self.current-frame; - return True - if $frame.properties.properties{$name} :exists; + return so (self.current-frame.properties.properties{$name} :exists); } method register-subhandler { @@ -263,8 +275,8 @@ class _007::Runtime { return $thing if $thing ~~ Val; - return $thing.new(:name($thing.name), :frame(NONE)) - if $thing ~~ Q::Identifier; + return Q::Term::Identifier.new(:name($thing.name)) + if $thing ~~ Q::Term::Identifier; return $thing if $thing ~~ Q::Unquote; @@ -618,6 +630,9 @@ class _007::Runtime { elsif $obj ~~ Val::Type && $obj.type === Q::Term && $propname eq "Array" { return Val::Type.of(Q::Term::Array); } + elsif $obj ~~ Val::Type && $obj.type === Q::Term && $propname eq "Identifier" { + return Val::Type.of(Q::Term::Identifier); + } else { if $obj ~~ Val::Type { die X::Property::NotFound.new(:$propname, :type("$type ({$obj.type.^name})")); diff --git a/t/api-documentation/comments.t b/t/api-documentation/comments.t index 25f61f13..c48a07b4 100644 --- a/t/api-documentation/comments.t +++ b/t/api-documentation/comments.t @@ -7,6 +7,8 @@ for -> $file { my $state = Normal; for $file.IO.lines -> $line { + next if $line ~~ / \h '{ ... }' $/; + if $line ~~ /^ < class role > \h+ (Q | < Val:: Q:: > \S+)/ { ok $state == ApiComment, "$0 is documented"; } diff --git a/t/features/hygiene.t b/t/features/hygiene.t new file mode 100644 index 00000000..48b3c0be --- /dev/null +++ b/t/features/hygiene.t @@ -0,0 +1,62 @@ +use v6; +use Test; +use _007::Test; + +{ + my $program = q:to/./; + macro moo() { + my x = "OH HAI"; + return quasi { + say(x); + } + } + + moo(); + . + + outputs + $program, + "OH HAI\n", + "quasis remember variables from their surrounding macro"; +} + +{ + my $program = q:to/./; + macro moo() { + return quasi { + my x = "OH HAI"; + say(x); + } + } + + moo(); + . + + outputs + $program, + "OH HAI\n", + "variables can be declared as usual inside of a quasi (and survive)"; +} + +{ + my $program = q:to/./; + my a = "OH"; + + macro moo(x) { + my a = "macro"; + return quasi { + {{{x}}} + } + } + + a = a ~ " HAI"; + say(moo(a)); + . + + outputs + $program, + "OH HAI\n", + "mainline variable survive all the way through a macro/quasi expansion"; +} + +done-testing; diff --git a/t/features/macros.t b/t/features/macros.t index adc3b345..898066b2 100644 --- a/t/features/macros.t +++ b/t/features/macros.t @@ -31,8 +31,8 @@ use _007::Test; my $program = q:to/./; macro foo() { return new Q.Postfix.Call { - identifier: new Q.Identifier { name: "postfix:()" }, - operand: new Q.Identifier { name: "say" }, + identifier: new Q.Term.Identifier { name: "postfix:()" }, + operand: new Q.Term.Identifier { name: "say" }, argumentlist: new Q.ArgumentList { arguments: [new Q.Literal.Str { value: "OH HAI" }] }