#!/usr/bin/env ruby
# $Id: ToSource.rb,v 1.2 2001/09/18 00:25:19 matju Exp $
=begin

	MetaRuby
	file: Generic unparser for Ruby

	Copyright (c) 2001 by Mathieu Bouchard
	Licensed under the same license as Ruby.

=end

#--------------------------------------------------------------------------#
# Issues
=begin

Eventually there should be two modes of operation: following
encapsulation (current ToSource) and breaking encapsulation (more like
Data::Dumper)

By breaking encapsulation you don't have to worry about causality
loops. This keeps Data::Dumper simple. By following encapsulation, you
have to break causality loops by constructing some objects in two
phases. (? or more phases ?)

I think that in most cases, if you follow encapsulation, you don't need
loops as much.

=end
#--------------------------------------------------------------------------#
# Exceptions

# for when some_class.construct isn't defined/meaningful
class NotConstructibleError < StandardError; end

# for when some_object.unconstruct isn't defined/meaningful
class NotDeconstructibleError < StandardError; end

# for when an operation cannot be done because of an object directly or
# indirectly linking to itself, e.g.:
#   a = []; a<<a         # self-loop
#   a = [b = []]; b<<a   # mutual loop
class CircularityError < TypeError; end

# for when an operation cannot be done because of an object directly or
# indirectly linking to twice the same target, e.g.:
#   a = []; b = [a,a]          # from the same array
#   a = [[[]]]; a[1] = a[0][0] # from different arrays
class MultipleLinkError < TypeError; end

#--------------------------------------------------------------------------#
# Extensions to builtin classes

class Object
	# This method should return an array of the shape [class,*arguments]
	# in which the first element is a Class (or Module) on which
	# #construct can be called with *arguments to recreate a
	# sufficiently equivalent object.
	# the default implementation raises an exception.
	def deconstruct
		raise NotDeconstructibleError.new(type.to_s)
	end

	def to_source
		output_stream = ""
		# output_stream = STDOUT
		using =
			RubySourceWriter
		#	LispSourceWriter
		tt = TreeTraversal.new(using.new(output_stream))
		tt.write self
		tt.writer.flush
		output_stream
	end
end

class Class
	# Creates a new object of a certain class from an *args part;
	# opposite of Object#deconstruct(). 
	def construct(*args)
		raise NotConstructibleError.new(type.to_s)
	end
	def deconstruct
		[Constant,to_s]
	end
end

module Constant
	def Constant.construct(symbol_name)
		case symbol_name
		when "nil"; nil
		when "true"; true
		when "false"; false
		else
			x = symbol_name.split(/::/)
			c = Object
			until x.empty?; c=c.const_get(x.shift); end
		end
	end
end

class   NilClass; def deconstruct; [Constant, "nil"  ]; end end
class  TrueClass; def deconstruct; [Constant, "true" ]; end end
class FalseClass; def deconstruct; [Constant, "false"]; end end

# usually pointless
class Integer
	def deconstruct; [type, to_s]; end
	def Integer.construct(*args); Integer(args[0]); end
end

# usually pointless
class Float
	def deconstruct; [type, to_s]; end
	def Float.construct(*args); Float(args[1]); end
end

# usually pointless
# does not handle frozen (should it?)
class String
	def deconstruct; [type, self]; end
	def String.construct(*args); args[0]; end
end

# does not handle frozen (should it?)
class Array
	def deconstruct; [type, *self]; end
	def Array.construct(*args); [*args]; end
end

# does not handle default value
# does not handle frozen (should it?)
class Hash
	def deconstruct; a=[type]; each_pair{|k,v| a << k << v}; a; end
	def Hash.construct(*args); Hash[*args]; end
end

class Symbol
	def deconstruct; [type, to_s]; end
	def Symbol.construct(*args)
		s = args[0]
		s =~ /^[A-Za-z_]+[\!\?\=]?$/ or
			raise ArgumentError.new "bad symbol name"
		eval(":"+s)
	end
end

class Regexp
	def deconstruct; [type, source]; end
	def Regexp.construct(*args)
		new(args)
	end
end

#--------------------------------------------------------------------------#

class TreeTraversal
	attr_accessor :writer
	def initialize(writer)
		self.writer=writer
		@seen={}  # to detect merges (eventually)
		@stack={} # to detect loops (eventually)
	end
	def exempt?(x)
		case x
		when true, false, nil; true
		when Integer, Symbol; true
		else false
		end
	end
	def write(x)
		if not exempt?(x) then
			raise CircularityError if @stack.has_key?(x.id)
			@stack[x.id] = x
			raise MultipleLinkError if @seen.has_key?(x.id)
			@seen[x.id] = x
		end
		foo = x.deconstruct
		unless Array===foo # and Class===foo[0]
			raise "deconstruction error for #{x}: #{foo.inspect}"
		end
		@writer.write(foo) {|y| write y }
		if not exempt?(x) then
			@stack.delete(x.id)
		end
	end
end

#--------------------------------------------------------------------------#
# SourceFormatter puts the "pretty" in pretty-printing.
# designed for text/plain, of course.

class SourceFormatter

	def initialize(target=STDOUT)
		self.target = target
		self.indent_depth = 2
		self.use_tab = false
		self.width = 72
		@lev=0
		@buf=[]
		@len=0
		@col=0
		@row=0
	end

# category: public interface

	# the victim IO instance
	attr_accessor :target

	# use_tab = true:
	#   each level is indented by a single tab;
	#   a tab is assumed to be worth #indent_depth spaces
	#   the tab character will not be used elsewhere
	# use_tab = false:
	#   each level is indented by #indent_depth spaces
	attr_accessor :use_tab
	attr_accessor :indent_depth

	# By default 72 characters. This value will be enforced except:
	#  * when indentation is too deep. half the value of #width
	#      is considered the minimum number of characters after indenting.
	#  * when a component simply cannot fit on its own line; in which case
	#      it is put on its own single line anyway.
	attr_accessor :width

	# prints (a string representing) data onto the stream.
	# if a block is given, it will be called if the data is too big to fit
	# on a line. its parameter will be the maximum desired number of
	# characters per line. The block should return an array of pieces
	# each no longer than that (well, as much as possible).
	# block not supported yet
	def add(data,&proc)
		#printf "add: %s\n",data.inspect
		@buf << [@lev,data]
		@len += data.length
		if @len > usable_width then
			t,d = @buf.shift
			tabs t; target << d; nl
			@len -= d.length
		end
	end

	# enter one level.
	# this has an effect on how elements are grouped together and this also
	# increases the indentation level.
	# when given a block, calls it and then calls #leave.
	def enter(a,b=nil,&proc)
		add a
		@lev += 1
		if proc then proc.call; leave(b); end
	end

	# opposite of enter
	def leave(b=nil)
		raise StandardError "already at top level" if @lev < 1
		k = @buf.length - 1
		while k>=0 and @buf[k][0] == @lev
			k -= 1
		end
		#!@#$ disabled for now
		if false and k>=0 then
			foo = @buf.slice!(k..@buf.length-1)
			foo.map!{|x| x[1] }
			@lev -= 1
			@buf << [@lev, foo.join("") + b]
		else 
			@lev -= 1
			add b
		end
	end

	def nl
		target << "\n"
		@row += 1; @col = 0
	end

	def space
		add " "
	end

# category: other

	def usable_width
		uw = width - indent_depth*@lev
		[uw,width/2].max
	end		

	# should only be used at the beginning of a line or after another tab.
	def tabs(n)
		target << if use_tab then "\t"*n else " "*indent_depth*n end
		@col += indent_depth*n
	end

	# call this when you're done
	def flush
		while @buf.length > 0 do
			t,d = @buf.shift
			tabs t; target << d; nl
			@len -= d.length
		end
	end
end

#--------------------------------------------------------------------------#
# The part common to all language modules

class SourceWriter
	attr_accessor :formatter
	def initialize(target)
		self.formatter = SourceFormatter.new(target)
	end
	
	def write(se,&callback)
		raise NotImplementedError
	end

	# delegators
	def add(data,&proc) ; formatter.add(data,&proc) end
	def space           ; formatter.space           end
	def flush           ; formatter.flush           end
	def enter(a,b,&proc); formatter.enter(a,b,&proc) end

end

#--------------------------------------------------------------------------#

# adhockery. haven't thought about it yet.
module WriterFeatures
	def initialize(target)
		super
		@on_add = []
	end

	def add(data,&proc)
		super(data + (@on_add.length>0 ? @on_add.pop : ""), &proc)
	end

	def enter(a,b,&proc)
		super(a,b + (@on_add.length>0 ? @on_add.pop : ""), &proc)
	end

	def on_add(data)
		@on_add.push data
	end
end

#--------------------------------------------------------------------------#
# LispSourceWriter corresponds to LGRAM 0.6 (for which there is no spec)
# TODO: make output LGRAM 0.7.pre3-conformant

class LispSourceWriter < SourceWriter
	include WriterFeatures
	def write(se,&callback)
		se = se.reverse
		t = se.pop
		if t == Constant or t <= Integer; add se.pop.to_s
		elsif t == Symbol; add ":"+se.pop.to_s
		elsif t == String; write_text se.pop.to_s
		else
			write_list(se,t,&callback)
		end
	end
	def write_text(text)
		a = text.gsub(/[\000-\037\177-\377"\\]/) {|x|
			if    x[0]==10 then "\\n"
			elsif x[0]==92 then "\\\\"
			elsif x[0]==34 then "\\\""
			else
				format("\\x%02x",x[0])
			end
		}
		add '"%s"'%[a]
	end

	def write_list(se,t,&callback)
		enter("("+type_to_s(t),")") {
			while se.length > 0
				callback.call(se.pop)
				on_add(if se.length==1 then "" else " " end)
				break if se.length == 0
			end
		}
	end
	def type_to_s(a_type)
		a_type.to_s
	end
end

#--------------------------------------------------------------------------#

class RubySourceWriter < SourceWriter
	include WriterFeatures
	def write(se,&callback)
		se = se.reverse
		t = se.pop
		if t == Constant or t <= Integer; add se.pop.to_s
		elsif t <= Symbol; add ":"+se.pop.to_s
		elsif t <= String; write_text se.pop.to_s
		elsif t == Array; write_list(se,"[","]",&callback)
		elsif t == Hash;  write_list(se,"{","}",&callback)
		elsif t == Regexp; add sprintf "/%s/", se.pop
		else
			# enter { write_list(se,type_to_s(t)+".construct(",")",&callback) }
			write_list(se,type_to_s(t)+".construct(",")",&callback)
		end
	end
	def write_list(se,start,stop,&callback)
		enter(start,stop) {
			while se.length > 0
				on_add(if se.length==1 then "" else ", " end)
				callback.call(se.pop)
				break if se.length == 0
			end
		}
	end
	def write_text(text)
		a = text.gsub(/[\000-\037\177-\377"\\]/) {|x|
			if    x[0]==10 then "\\n"
			elsif x[0]==92 then "\\\\"
			elsif x[0]==34 then "\\\""
			else
				format("\\x%02x",x[0])
			end
		}
		add '"%s"'%[a]
	end
	def type_to_s(a_type)
		a_type.to_s
	end
end

#--------------------------------------------------------------------------#
