use NQPHLL; # Ruby subset extended from the `rubyish` example, as introduced in the # Edument Rakudo and NQP internals course. my %CLASSES; class RubyishClassHOW { has $!name; has $!isa; has %!methods; #| define a new class method new_type(:$name!, :$isa?) { nqp::die("duplicate class definition: $name") if %CLASSES{ $name }; my $obj := self.new(:$name, :$isa); %CLASSES{ $name } := [$obj]; nqp::newtype($obj, 'HashAttrStore'); } #| add a named method to a class method add_method($obj, $name, $code) { nqp::die("This class already has a method named " ~ $name) if nqp::existskey(%!methods, $name); %!methods{$name} := $code; } #| find a named method in a class or its parents #| a '^' prefix, skips the current class, starting at the parent method find_method($obj, $name) { my $method; if nqp::substr($name, 0, 1) eq '^' { $name := nqp::substr($name, 1); } else { $method := %!methods{$name}; } if !$method && $!isa { my $super := %CLASSES{ $!isa }; nqp::die("unresolved super-class: " ~ $!isa) unless $super; $method := $super[0].find_method( $obj, $name); } $method // nqp::null(); } } grammar Rubyish::Grammar is HLL::Grammar { token TOP { :my $*CUR_BLOCK := QAST::Block.new(QAST::Stmts.new()); :my $*TOP_BLOCK := $*CUR_BLOCK; # global top-level block :my $*CLASS_BLOCK := $*CUR_BLOCK; # out class block :my $*IN_TEMPLATE := 0; # true, if in a template :my $*IN_PARENS := 0; # true, if in a parentheised list (signature etc) :my %*SYM; # symbols in current scope :my %*SYM-GBL; # globals and package variables :my %*SYM-CLASS; # class-inherited methods ^ ~ $ || <.panic('Syntax error')> } rule separator { ';' | \n } token continuation { \\ \n } #| a list of statements and/or template expressions rule stmtlist { [ ? ] *%% [<.separator>|] } #| a single statement, plus optional modifier token stmtish {:s [ ]? } token modifier {if|unless|while|until} proto token stmt {*} #| function, or method definition token stmt:sym {:s :my %*inner-sym := nqp::clone(%*SYM); :my $*DEF; 'def' ~ 'end' { %*SYM{$*DEF} := %*inner-sym{$*DEF}; } } rule defbody { :my %*SYM := %*inner-sym; :my $*CUR_BLOCK := QAST::Block.new(QAST::Stmts.new()); { $*DEF := ~$; if $*IN_CLASS { # if we're in a class, we're defining a method ... %*SYM{$*DEF} := 'method'; %*SYM := 'var'; } else { # ... otherwise it's a function %*SYM{$*DEF} := 'func'; } } ['(' ~ ')' ?]? ? } rule comma { [','|'=>'] } #| a signature; for a method, function or closure rule signature { :my $*IN_PARENS := 1; [ | '*' | '&' ] +% ',' } token param {:s [ $=':' ? | '=' ]? { %*SYM{~$} := 'var' } } #| a class definition token stmt:sym { :my $*IN_CLASS := 1; :my @*METHODS; :my %*inner-sym := nqp::clone(%*SYM); [ \h+] ~ [\h* 'end'] } rule classbody { :my %*SYM := %*inner-sym; :my $*CUR_BLOCK := QAST::Block.new(QAST::Stmts.new()); :my $*CLASS_BLOCK := $*CUR_BLOCK; { $*CLASS_BLOCK.name(~$) } [ '<' { inherit-syms(~$) } ]? { %*SYM-CLASS{~$} := %*SYM; } } token stmt:sym { } token term:sym {:s '=' } token code-block {:s :my $*CUR_BLOCK := QAST::Block.new(QAST::Stmts.new()); } our %builtins; #| functions that map directly to nqp ops BEGIN { %builtins := nqp::hash( 'abort', 'die', 'exit', 'exit', 'key', 'iterkey_s', 'delete', 'deletekey', 'value', 'iterval', 'print', 'print', 'puts', 'say', 'sleep', 'sleep', ); } #| a call to a function or method token term:sym { ['(' ~ ')' ? ? |:s )}> ? ] } #| a call to the super-method (aka callsame) token term:sym { 'super' ['(' ~ ')' ? ? |:s ? ] [ || <.panic("'super' call outside of method")> ] } #| call to an nqp operation token term:sym { 'nqp::' ['(' ~ ')' ? | ? ] } #| quoted words, e.g.: %w token term:sym { \%w } token call-args {:s [ ] +% ',' } proto token arg {*} token arg:sym {:s '|':']> } token arg:sym { '&' } token arg:sym { ':' } token arg:sym {:s [ '=>' ]+ % ',' } token paren-args {:my $*IN_PARENS := 1; } token operation {[\!|\?]?} token term:sym { ['new' \h+ | '.' 'new'] ['(' ~ ')' ?]? } # process a variable name, e.g.: localvar $global @attr Pkg::Var # the first reference to a local or global variable must be an assignment token var { :my $*MAYBE_DECL := 0; \+? $=[ $=[ \$ | \@ ] | [ ? '::' ]? > || \( ]> ] [ { $*MAYBE_DECL := 1 }> || ) || ~$ eq '@' }> || ) }> <.panic("unknown variable or method: $")> ] } token term:sym { } token term:sym { \+? } proto token value {*} token value:sym {} token strings {:s ? } token string { # 'non-interpolating' | # "interpolating#{42}" | \%[ q # %q | Q # %Q ] } token value:sym {'<<'} proto token heredoc {*} #| non-interpolating heredoc: <<'MARKER' ... MARKER token heredoc:sym {[$=<.ident> | \' $=.+? \' ]\n $=.*? \n$$$ } #| interpolating heredoc: <<"MARKER" ... MARKER token heredoc:sym {\" $=.+? \" \n [ | ]*? \n$$$ } token heredoc-line {\n? [ \N]+ | \n } #| Interpolation token interp { '#{' ~ '}' [ [:s ] || ] } token quote_escape:sym<#{ }> { } token paren-list { :my $*IN_PARENS := 1; *%% } token value:sym { } token value:sym { } token value:sym {'[' ~ ']' } token value:sym {'{' ~ '}' } token value:sym { } token value:sym { } token value:sym { } # Reserved words. token keyword { [ BEGIN | class | ensure | nil | new | when | END | def | false | not | super | while | alias | defined | for | or | then | yield | and | do | if | redo | true | begin | else | in | rescue | undef | break | elsif | module | retry | unless | case | end | next | return | until | eq | ne | lt | gt | le | ge | cmp ] } ## Operator precedence levels # -- see http://www.tutorialspoint.com/ruby/ruby_operators.htmw my %methodop := nqp::hash('prec', 'y=', 'assoc', 'unary'); # y: ** my %exponentiation := nqp::hash('prec', 'y=', 'assoc', 'left'); # x: ! ~ + - (unary) my %unary := nqp::hash('prec', 'x=', 'assoc', 'unary'); # w: * / % my %multiplicative := nqp::hash('prec', 'w=', 'assoc', 'left'); # u: + - my %additive := nqp::hash('prec', 'u=', 'assoc', 'left'); # t: >> << my %bitshift := nqp::hash('prec', 't=', 'assoc', 'left'); # s: & my %bitand := nqp::hash('prec', 's=', 'assoc', 'left'); # r: ^ | my %bitor := nqp::hash('prec', 'r=', 'assoc', 'left'); # q: <= < > >= le lt gt ge my %comparison := nqp::hash('prec', 'q=', 'assoc', 'left'); # n: <=> == === != =~ !~ eq ne cmp my %equality := nqp::hash('prec', 'n=', 'assoc', 'left'); # l: && my %logical_and := nqp::hash('prec', 'l=', 'assoc', 'left'); # k: || my %logical_or := nqp::hash('prec', 'k=', 'assoc', 'left'); # q: ?: my %conditional := nqp::hash('prec', 'g=', 'assoc', 'right'); # f: = %= { /= -= += |= &= >>= <<= *= &&= ||= **= my %assignment := nqp::hash('prec', 'f=', 'assoc', 'right'); # e: not (unary) my %loose_not := nqp::hash('prec', 'e=', 'assoc', 'unary'); # c: or and my %loose_logical := nqp::hash('prec', 'c=', 'assoc', 'left'); # Operators - mostly stolen from NQP token infix:sym<**> { )> } token prefix:sym<-> { ]> )> } token prefix:sym { )> } token infix:sym<*> { )> } token infix:sym { )> } token infix:sym<%> { ]> )> } token infix:sym<+> { )> } token infix:sym<-> { )> } token infix:sym<~> { )> } token infix:sym«<<» { )> } token infix:sym«>>» { )> } token infix:sym<&> { )> } token infix:sym<|> { )> } token infix:sym<^> { )> } token infix:sym«<=» { ]> )> } token infix:sym«>=» { )> } token infix:sym«<» { )> } token infix:sym«>» { )> } token infix:sym«le» { )> } token infix:sym«ge» { )> } token infix:sym«lt» { )> } token infix:sym«gt» { )> } token infix:sym«==» { )> } token infix:sym«!=» { )> } token infix:sym«<=>» { )> } token infix:sym«eq» { )> } token infix:sym«ne» { )> } token infix:sym«cmp» { )> } token infix:sym<&&> { )> } token infix:sym<||> { )> } token infix:sym {:s '?' ':' , :op)> } token bind-op {'='=]>} token infix:sym<=> { <.bind-op> )> } token prefix:sym { )> } token infix:sym { )> } token infix:sym { )> } # Parenthesis token circumfix:sym<( )> { :my $*IN_PARENS := 1; '(' ~ ')' } # Method call token postfix:sym<.> { '.' [ '(' ~ ')' ? ]? } # Array and hash indices token postcircumfix:sym<[ ]> { '[' ~ ']' [ ] } token postcircumfix:sym<{ }> { '{' ~ '}' [ ] } token postcircumfix:sym { } # Statement control rule xblock {:s [ 'then'? | 'then' | >] } token stmt:sym {:s $=[if|unless] ~ 'end' [ [|]? ] } token elsif {:s 'elsif' ~ [|]? } token else {:s 'else' } token stmt:sym {:s $=[while|until] } token stmt:sym {:s 'in' { %*SYM{~$} := 'var' } } token do-block { ~ 'end' } token do {:s 'do'? | 'do' | > } token term:sym { 'begin' ~ 'end' } token closure {:s ['{' ['|' ~ '|' ?]? ] ~ '}' |:s ['do' ['|' ~ '|' ?]? ] ~ 'end' } token closure2 {:s ['(' ~ ')' ? ]? '{' ~ '}' } token term:sym {:s :my $*CUR_BLOCK := QAST::Block.new(QAST::Stmts.new()); ['lambda' | '->' ] } # Comments and whitespace proto token comment {*} token comment:sym { '#' [ \N* || [>\N]*] } token comment:sym {[^^'=begin'\n] [ .*? [^^'=end'[\n|$]] || <.panic('missing ^^=end at eof')>] } token ws { [\h | <.continuation> | <.comment> | \n]* } token hs { [\h | <.continuation> ]* } # Templates token template-chunk { [|] ~ [|$] * } proto token template-nibble {*} token template-nibble:sym { } token template-nibble:sym { [<.tmpl-unesc>|<.tmpl-hdr>] <.panic("Stray tag, e.g. '%>' or ''")> } token template-nibble:sym { [|'#{'|$]> .]+ } token tmpl-hdr {'' \h* \n? {$*IN_TEMPLATE := 1} } token tmpl-esc {\h* '<%' [ || <.panic('Template directive precedes ""')>] } token tmpl-unesc { '%>' \h* \n? [ || <.panic('Template directive precedes ""')>] } # Functions sub callable($op) { my $type := %*SYM{$op} // (%builtins{$op} && 'func'); $type && ($type eq 'func' || $type eq 'method'); } sub variable($op) { my $type := %*SYM{$op} // %*SYM-GBL{$op}; $type && ($type eq 'var'); } sub inherit-syms($class) { if my %syms := %*SYM-CLASS{$class} { %*SYM{$_} := %syms{$_} for %syms; } } } class Rubyish::Actions is HLL::Actions { method TOP($/) { $*CUR_BLOCK.push($.ast); make QAST::CompUnit.new( $*CUR_BLOCK ); } method stmtlist($/) { my $stmts := QAST::Stmts.new( :node($/) ); $stmts.push($_.ast) for @; make $stmts; } method stmtish($/) { make $ ?? QAST::Op.new( $.ast, $.ast, :op(~$), :node($/) ) !! $.ast; } method term:sym($/) { make $.ast; } method code-block($/) { make $.ast } method term:sym($/) { my $name := ~$; my $op := %Rubyish::Grammar::builtins{$name}; my $call; if $op { $call := QAST::Op.new( :op($op) ) } elsif %*SYM{$name} eq 'method' && $*DEF { $call := QAST::Op.new( :op('callmethod'), QAST::Var.new( :name('self'), :scope('lexical')), QAST::SVal.new( :value($name) ), ); } else { $call := QAST::Op.new( :op('call'), :name($name) ); } if $ { $call.push($_) for $.ast; } $call.push( $.ast ) if $; make $call; } method term:sym($/) { my $name := ~$*DEF; my $call := QAST::Op.new( :op('callmethod'), QAST::Var.new( :name('self'), :scope('lexical')), QAST::SVal.new( :value('^' ~ $name) ), ); if $ { $call.push($_) for $.ast; } $call.push( $.ast ) if $; make $call; } method term:sym($/) { my $op := ~$; my $call := QAST::Op.new( :op($op) ); if $ { $call.push($_) for $.ast; } make $call; } method call-args($/) { my @args; @args.push($_.ast) for $; make @args; } method arg:sym($/) { make $.ast } method arg:sym($/) { make $.ast } method arg:sym($/) { my $arg := $.ast; $arg.named( ~$ ); make $arg; } method arg:sym($/) { my $args := QAST::Op.new( :op ); $args.push( $_.ast ) for $; make $args; } method paren-args($/) { make $.ast; } method term:sym($/) { my $tmp-obj := '$new-obj$'; my $init-call := QAST::Op.new( :op, :name, QAST::Var.new( :name($tmp-obj), :scope ) ); if $ { $init-call.push($_) for $.ast; } my $init-block := QAST::Block.new( QAST::Stmts.new( # pseudo-code: # # def new(*call-args) # $new-obj = Class.new; # if call-args then # # always try to call initialize, when new has arguments # $new-obj.initialize(call-args) # else # $new-obj.initialize() \ # if $new-obj.can('initialize') # end # return $new-obj # end # create the new object QAST::Op.new( :op('bind'), QAST::Var.new( :name($tmp-obj), :scope, :decl), QAST::Op.new( :op('create'), QAST::Var.new( :name('::' ~ ~$), :scope('lexical') ) ), ), # call initialize method, if available ($ ?? $init-call !! QAST::Op.new( :op, QAST::Op.new( :op, QAST::Var.new( :name($tmp-obj), :scope ), QAST::SVal.new( :value )), $init-call, ) ), # return the new object QAST::Var.new( :name($tmp-obj), :scope ), )); $init-block.blocktype('immediate'); make $init-block; } method var($/) { my $sigil := ~$ // ''; my $name := ~$; if $sigil eq '@' && $*IN_CLASS && $*DEF { # instance variable, bound to self my $package-name := $*CLASS_BLOCK.name; make QAST::Var.new( :name($name), :scope('attribute'), QAST::Var.new( :name('self'), :scope('lexical')), QAST::SVal.new( :value($package-name) ) ); } else { if $ { my $ns := $ ?? ~$ !! $*CLASS_BLOCK.name; $name := $ns ~ '::' ~ $; } if $*MAYBE_DECL { my $block; my $decl := 'var'; if $sigil eq '$' || $ { $block := $*TOP_BLOCK; %*SYM-GBL{$name} := 'var'; %*SYM{~$} := 'var' if $; } elsif $sigil eq '@' { $block := $*CLASS_BLOCK; } else { $block := $*CUR_BLOCK; } my %sym := $block.symbol($name); if !%sym { %*SYM{$name} := 'var'; $block.symbol($name, :declared(1), :scope('lexical') ); my $var := QAST::Var.new( :name($name), :scope('lexical'), :decl($decl) ); $block[0].push($var); } } make QAST::Var.new( :name($name), :scope('lexical') ); } } method term:sym($/) { make $.ast } method stmt:sym($/) { my $install := $.ast; $*CUR_BLOCK[0].push(QAST::Op.new( :op('bind'), QAST::Var.new( :name($install.name), :scope('lexical'), :decl('var') ), $install )); if $*IN_CLASS { @*METHODS.push($install); } make QAST::Op.new( :op('null') ); } method defbody($/) { $*CUR_BLOCK.name(~$); $*CUR_BLOCK.push($.ast); if $*IN_CLASS { # it's a method, self will be automatically passed $*CUR_BLOCK[0].unshift(QAST::Var.new( :name('self'), :scope('lexical'), :decl('param') )); $*CUR_BLOCK.symbol('self', :declared(1)); } make $*CUR_BLOCK; } method param($/) { my $var := QAST::Var.new( :name(~$), :scope('lexical'), :decl('param') ); $var.named(~$) if $; $*CUR_BLOCK.symbol('self', :declared(1)); $var.default( $.ast ) if $; make $var; } method signature($/) { my @params; @params.push($_.ast) for @; if $ { @params.push($[0].ast); @params[-1].slurpy(1); } if $ { @params.push($[0].ast); @params[-1].default(QAST::Op.new( :op )); } for @params { $*CUR_BLOCK[0].push($_) unless $_.named; $*CUR_BLOCK.symbol($_.name, :declared(1)); } # nqp #179 named arguments need to follow positional parameters for @params { $*CUR_BLOCK[0].push($_) if $_.named; } } method stmt:sym($/) { my $body_block := $.ast; # Generate code to create the class. my $class_stmts := QAST::Stmts.new( $body_block ); my $ins_name := '::' ~ $; my $new_type := QAST::Op.new( :op('callmethod'), :name('new_type'), QAST::WVal.new( :value(RubyishClassHOW) ), QAST::SVal.new( :value(~$), :named('name') ), ); $new_type.push( QAST::SVal.new( :value(~$), :named('isa') ) ) if $; $class_stmts.push(QAST::Op.new( :op('bind'), QAST::Var.new( :name($ins_name), :scope('lexical'), :decl('var') ), $new_type, )); # Add methods. my $class_var := QAST::Var.new( :name($ins_name), :scope('lexical') ); for @*METHODS { my $name := $_.name; $class_stmts.push(QAST::Op.new( :op('callmethod'), :name('add_method'), QAST::Op.new( :op('how'), $class_var ), $class_var, QAST::SVal.new( :value($name) ), QAST::BVal.new( :value($_) )) ); } make $class_stmts; } method classbody($/) { $*CUR_BLOCK.push($.ast); $*CUR_BLOCK.blocktype('immediate'); make $*CUR_BLOCK; } method stmt:sym($/) { make $.ast; } method term:sym($/) { my $op := $.made; make QAST::Op.new( :op('bind'), $.ast, QAST::Op.new( :op($op), $.ast, $.ast )); } method value:sym($/) { make $.ast; } method strings($/) { make $ ?? QAST::Op.new( :op('concat'), $.ast, $.ast) !! $.ast; } method string($/) { make $.ast; } method value:sym($/) { make $.ast } method heredoc:sym($/) { make QAST::SVal.new( :value( ~$ ) ); } method heredoc-line($/) { make QAST::SVal.new( :value(~$/) ) } method heredoc:sym($/) { my $value := QAST::SVal.new( :value('') ); $value := QAST::Op.new( :op, $value, $_.ast) for $; make $value; } method value:sym($/) { my $value := $.ast; make QAST::IVal.new( :value($value) ) } method value:sym($/) { my $value := $.ast; make QAST::NVal.new( :value($value) ) } method paren-list($/) { my @list; if $ { @list.push($_.ast) for $ } make @list; } method value:sym($/) { my $array := QAST::Op.new( :op ); $array.push($_) for $.ast; make $array; } method term:sym($/) { make $.ast; } method value:sym($/) { my $hash := QAST::Op.new( :op ); $hash.push($_) for $.ast; make $hash; } method value:sym($/) { make QAST::Op.new( :op ); } method value:sym($/) { make QAST::IVal.new( :value(1) ); } method value:sym($/) { make QAST::IVal.new( :value(0) ); } method interp($/) { make $.ast } method quote_escape:sym<#{ }>($/) { make $.ast } method circumfix:sym<( )>($/) { make $.ast } # todo: proper type objects our %call-tab; BEGIN { %call-tab := nqp::hash( 'call', 'call', 'nil?', 'isnull' ) } method postfix:sym<.>($/) { my $op := %call-tab{ ~$ }; my $meth_call := $op ?? QAST::Op.new( :op($op) ) !! QAST::Op.new( :op('callmethod'), :name(~$) ); if $ { $meth_call.push($_) for $.ast; } make $meth_call; } method postcircumfix:sym<[ ]>($/) { make QAST::Var.new( :scope('positional'), $.ast ); } method postcircumfix:sym<{ }>($/) { make QAST::Var.new( :scope('associative'), $.ast ); } method postcircumfix:sym($/) { make QAST::Var.new( :scope('associative'), $.ast ); } method xblock($/) { make QAST::Op.new( $.ast, $.ast, :node($/) ); } method stmt:sym($/) { my $ast := $.ast; $ast.op( ~$ ); $ast.push( $.ast ) if $; make $ast; } method elsif($/) { my $ast := $.ast; $ast.op( 'if' ); $ast.push( $.ast ) if $; make $ast; } method else($/) { make $.ast } method stmt:sym($/) { make QAST::Op.new( $.ast, $.ast, :op(~$), :node($/) ); } method stmt:sym($/) { my $block := QAST::Block.new( QAST::Var.new( :name(~$), :scope('lexical'), :decl('param')), $.ast, ); make QAST::Op.new( $.ast, $block, :op('for'), :node($/) ); } method do-block($/) { make $.ast } method term:sym($/) { make $.ast; } method closure($/) { $*CUR_BLOCK.push($.ast); make QAST::Op.new(:op, $*CUR_BLOCK ); } method closure2($/) { self.closure($/) } method term:sym($/) { make $.ast } method template-chunk($/) { my $text := QAST::Stmts.new( :node($/) ); $text.push( QAST::Op.new( :op, $_.ast ) ) for $; make $text; } method template-nibble:sym($/) { make $.ast } method template-nibble:sym($/) { make QAST::SVal.new( :value(~$/) ); } } class Rubyish::Compiler is HLL::Compiler { method eval($code, *@_args, *%adverbs) { my $output := self.compile($code, :compunit_ok(1), |%adverbs); if %adverbs eq '' { my $outer_ctx := %adverbs; $output := self.backend.compunit_mainline($output); if nqp::defined($outer_ctx) { nqp::forceouterctx($output, $outer_ctx); } $output := $output(); } $output; } } sub MAIN(*@ARGS) { my $comp := Rubyish::Compiler.new(); $comp.language('rubyish'); $comp.parsegrammar(Rubyish::Grammar); $comp.parseactions(Rubyish::Actions); $comp.command_line(@ARGS, :encoding('utf8')); }