Author: [email protected]
Date: Wed Jan 7 03:35:09 2009
New Revision: 1037
Added:
branches/experimental/toiger/test/mjsunit/multiple-return.js
Modified:
branches/experimental/toiger/src/codegen-ia32.cc
branches/experimental/toiger/src/codegen-ia32.h
branches/experimental/toiger/src/jump-target-ia32.cc
branches/experimental/toiger/src/virtual-frame-ia32.cc
branches/experimental/toiger/src/virtual-frame-ia32.h
branches/experimental/toiger/test/mjsunit/debug-evaluate.js
branches/experimental/toiger/test/mjsunit/debug-step.js
Log:
Enable register allocation for return statements. Make sure to
prepare the frame for the return even in the try-finally and
try-catch cases that shadows the function return label.
Review URL: http://codereview.chromium.org/16567
Modified: branches/experimental/toiger/src/codegen-ia32.cc
==============================================================================
--- branches/experimental/toiger/src/codegen-ia32.cc (original)
+++ branches/experimental/toiger/src/codegen-ia32.cc Wed Jan 7 03:35:09
2009
@@ -83,7 +83,6 @@
allocator_(NULL),
cc_reg_(no_condition),
state_(NULL),
- is_inside_try_(false),
break_stack_height_(0),
loop_nesting_(0),
function_return_is_shadowed_(false),
@@ -1627,41 +1626,58 @@
void CodeGenerator::VisitReturnStatement(ReturnStatement* node) {
ASSERT(!in_spilled_code());
- VirtualFrame::SpilledScope spilled_scope(this);
Comment cmnt(masm_, "[ ReturnStatement");
- CodeForStatement(node);
- LoadAndSpill(node->expression());
-
- // Move the function result into eax
- frame_->EmitPop(eax);
- // If we're inside a try statement or the return instruction
- // sequence has been generated, we just jump to that
- // point. Otherwise, we generate the return instruction sequence and
- // bind the function return label.
- if (is_inside_try_ || function_return_.is_bound()) {
+ if (function_return_is_shadowed_) {
+ // If the function return is shadowed, we spill all information
+ // and just jump to the label.
+ VirtualFrame::SpilledScope spilled_scope(this);
+ CodeForStatement(node);
+ LoadAndSpill(node->expression());
+ frame_->EmitPop(eax);
function_return_.Jump();
} else {
- function_return_.Bind();
- if (FLAG_trace) {
- frame_->EmitPush(eax); // undo the pop(eax) from above
- frame_->CallRuntime(Runtime::kTraceExit, 1);
+ // Load the returned value.
+ CodeForStatement(node);
+ Load(node->expression());
+
+ // Pop the result from the frame and prepare the frame for
+ // returning thus making it easier to merge.
+ Result result = frame_->Pop();
+ frame_->PrepareForReturn();
+
+ // Move the result into register eax where it belongs.
+ result.ToRegister(eax);
+ // TODO(): Instead of explictly calling Unuse on the result, it
+ // might be better to pass the result to Jump and Bind below.
+ result.Unuse();
+
+ // If the function return label is already bound, we reuse the
+ // code by jumping to the return site.
+ if (function_return_.is_bound()) {
+ function_return_.Jump();
+ } else {
+ function_return_.Bind();
+ if (FLAG_trace) {
+ frame_->Push(eax); // Materialize result on the stack.
+ frame_->CallRuntime(Runtime::kTraceExit, 1);
+ }
+
+ // Add a label for checking the size of the code used for returning.
+ Label check_exit_codesize;
+ __ bind(&check_exit_codesize);
+
+ // Leave the frame and return popping the arguments and the
+ // receiver.
+ frame_->Exit();
+ __ ret((scope_->num_parameters() + 1) * kPointerSize);
+ DeleteFrame();
+
+ // Check that the size of the code used for returning matches what is
+ // expected by the debugger.
+ ASSERT_EQ(Debug::kIa32JSReturnSequenceLength,
+ __ SizeOfCodeGeneratedSince(&check_exit_codesize));
}
-
- // Add a label for checking the size of the code used for returning.
- Label check_exit_codesize;
- __ bind(&check_exit_codesize);
-
- // Leave the frame and return popping the arguments and the
- // receiver.
- frame_->Exit();
- __ ret((scope_->num_parameters() + 1) * kPointerSize);
- DeleteFrame();
-
- // Check that the size of the code used for returning matches what is
- // expected by the debugger.
- ASSERT_EQ(Debug::kIa32JSReturnSequenceLength,
- __ SizeOfCodeGeneratedSince(&check_exit_codesize));
}
}
@@ -2310,17 +2326,21 @@
// target.
int nof_escapes = node->escaping_targets()->length();
List<ShadowTarget*> shadows(1 + nof_escapes);
+
+ // Add the shadow target for the function return.
+ static const int kReturnShadowIndex = 0;
shadows.Add(new ShadowTarget(&function_return_));
+ bool function_return_was_shadowed = function_return_is_shadowed_;
+ function_return_is_shadowed_ = true;
+ ASSERT(shadows[kReturnShadowIndex]->original_target() ==
&function_return_);
+
+ // Add the remaining shadow targets.
for (int i = 0; i < nof_escapes; i++) {
shadows.Add(new ShadowTarget(node->escaping_targets()->at(i)));
}
- bool function_return_was_shadowed = function_return_is_shadowed_;
- function_return_is_shadowed_ = true;
// Generate code for the statements in the try block.
- { TempAssign<bool> temp(&is_inside_try_, true);
- VisitStatementsAndSpill(node->try_block()->statements());
- }
+ VisitStatementsAndSpill(node->try_block()->statements());
// Stop the introduced shadowing and count the number of required
unlinks.
// After shadowing stops, the original targets are unshadowed and the
@@ -2377,6 +2397,10 @@
frame_->EmitPop(Operand::StaticVariable(handler_address));
frame_->Drop(StackHandlerConstants::kSize / kPointerSize - 1);
// next_sp popped.
+
+ if (!function_return_is_shadowed_ && i == kReturnShadowIndex) {
+ frame_->PrepareForReturn();
+ }
shadows[i]->original_target()->Jump();
}
}
@@ -2423,17 +2447,21 @@
// target.
int nof_escapes = node->escaping_targets()->length();
List<ShadowTarget*> shadows(1 + nof_escapes);
+
+ // Add the shadow target for the function return.
+ static const int kReturnShadowIndex = 0;
shadows.Add(new ShadowTarget(&function_return_));
+ bool function_return_was_shadowed = function_return_is_shadowed_;
+ function_return_is_shadowed_ = true;
+ ASSERT(shadows[kReturnShadowIndex]->original_target() ==
&function_return_);
+
+ // Add the remaining shadow targets.
for (int i = 0; i < nof_escapes; i++) {
shadows.Add(new ShadowTarget(node->escaping_targets()->at(i)));
}
- bool function_return_was_shadowed = function_return_is_shadowed_;
- function_return_is_shadowed_ = true;
// Generate code for the statements in the try block.
- { TempAssign<bool> temp(&is_inside_try_, true);
- VisitStatementsAndSpill(node->try_block()->statements());
- }
+ VisitStatementsAndSpill(node->try_block()->statements());
// Stop the introduced shadowing and count the number of required
unlinks.
// After shadowing stops, the original targets are unshadowed and the
@@ -2459,13 +2487,14 @@
// have been jumped to.
for (int i = 0; i <= nof_escapes; i++) {
if (shadows[i]->is_linked()) {
- // Because we can be jumping here (to spilled code) from unspilled
- // code, we need to reestablish a spilled frame at this block.
+ // Because we can be jumping here (to spilled code) from
+ // unspilled code, we need to reestablish a spilled frame at
+ // this block.
shadows[i]->Bind();
frame_->SpillAll();
- if (shadows[i]->original_target() == &function_return_) {
- // If this target shadowed the function return, materialize the
- // return value on the stack.
+ if (i == kReturnShadowIndex) {
+ // If this target shadowed the function return, materialize
+ // the return value on the stack.
frame_->EmitPush(eax);
} else {
// Fake TOS for targets that shadowed breaks and continues.
@@ -2518,11 +2547,20 @@
frame_->EmitPop(eax);
// Generate code to jump to the right destination for all used
- // (formerly) shadowing targets.
+ // formerly shadowing targets.
for (int i = 0; i <= nof_escapes; i++) {
if (shadows[i]->is_bound()) {
+ JumpTarget* original = shadows[i]->original_target();
__ cmp(Operand(ecx), Immediate(Smi::FromInt(JUMPING + i)));
- shadows[i]->original_target()->Branch(equal);
+ if (!function_return_is_shadowed_ && i == kReturnShadowIndex) {
+ JumpTarget skip(this);
+ skip.Branch(not_equal);
+ frame_->PrepareForReturn();
+ original->Jump();
+ skip.Bind();
+ } else {
+ original->Branch(equal);
+ }
}
}
@@ -4300,18 +4338,13 @@
}
-bool CodeGenerator::IsActualFunctionReturn(JumpTarget* target) {
- return (target == &function_return_ && !function_return_is_shadowed_);
-}
-
-
#ifdef DEBUG
bool CodeGenerator::HasValidEntryRegisters() {
- return (allocator()->count(eax) - frame()->register_count(eax) == 0)
- && (allocator()->count(ebx) - frame()->register_count(ebx) == 0)
- && (allocator()->count(ecx) - frame()->register_count(ecx) == 0)
- && (allocator()->count(edx) - frame()->register_count(edx) == 0)
- && (allocator()->count(edi) - frame()->register_count(edi) == 0);
+ return (allocator()->count(eax) == frame()->register_count(eax))
+ && (allocator()->count(ebx) == frame()->register_count(ebx))
+ && (allocator()->count(ecx) == frame()->register_count(ecx))
+ && (allocator()->count(edx) == frame()->register_count(edx))
+ && (allocator()->count(edi) == frame()->register_count(edi));
}
#endif
Modified: branches/experimental/toiger/src/codegen-ia32.h
==============================================================================
--- branches/experimental/toiger/src/codegen-ia32.h (original)
+++ branches/experimental/toiger/src/codegen-ia32.h Wed Jan 7 03:35:09 2009
@@ -454,10 +454,6 @@
void CodeForStatement(Node* node);
void CodeForSourcePosition(int pos);
- // Is the given jump target the actual (ie, non-shadowed) function return
- // target?
- bool IsActualFunctionReturn(JumpTarget* target);
-
bool is_eval_; // Tells whether code is generated for eval.
#ifdef DEBUG
@@ -478,7 +474,6 @@
RegisterAllocator* allocator_;
Condition cc_reg_;
CodeGenState* state_;
- bool is_inside_try_;
int break_stack_height_;
int loop_nesting_;
Modified: branches/experimental/toiger/src/jump-target-ia32.cc
==============================================================================
--- branches/experimental/toiger/src/jump-target-ia32.cc (original)
+++ branches/experimental/toiger/src/jump-target-ia32.cc Wed Jan 7
03:35:09 2009
@@ -69,20 +69,12 @@
ASSERT(cgen_->HasValidEntryRegisters());
if (expected_frame_ == NULL) {
- // The frame at the actual function return will always have height
zero.
- if (cgen_->IsActualFunctionReturn(this)) {
- current_frame->Forget(current_frame->height());
- }
current_frame->MakeMergable();
expected_frame_ = current_frame;
ASSERT(cgen_->HasValidEntryRegisters());
cgen_->SetFrame(NULL);
} else {
- // No code needs to be emitted to merge to the expected frame at the
- // actual function return.
- if (!cgen_->IsActualFunctionReturn(this)) {
- current_frame->MergeTo(expected_frame_);
- }
+ current_frame->MergeTo(expected_frame_);
ASSERT(cgen_->HasValidEntryRegisters());
cgen_->DeleteFrame();
}
@@ -114,10 +106,6 @@
if (expected_frame_ == NULL) {
expected_frame_ = new VirtualFrame(current_frame);
- // The frame at the actual function return will always have height
zero.
- if (cgen_->IsActualFunctionReturn(this)) {
- expected_frame_->Forget(expected_frame_->height());
- }
// For a branch, the frame at the fall-through basic block (not
labeled)
// does not need to be mergable, but only the other (labeled) one.
That
// is achieved by reversing the condition and emitting the make
mergable
@@ -135,29 +123,22 @@
__ j(cc, &label_, hint);
}
} else {
- // No code needs to be emitted to merge to the expected frame at the
- // actual function return.
- if (cgen_->IsActualFunctionReturn(this)) {
- ASSERT(cgen_->HasValidEntryRegisters());
- __ j(cc, &label_, hint);
- } else {
- // We negate the condition and emit the code to merge to the expected
- // frame immediately.
- //
- // TODO(): This should be replaced with a solution that emits the
- // merge code for forward CFG edges at the appropriate entry to the
- // target block.
- Label original_fall_through;
- __ j(NegateCondition(cc), &original_fall_through, NegateHint(hint));
- VirtualFrame* working_frame = new VirtualFrame(current_frame);
- cgen_->SetFrame(working_frame);
- working_frame->MergeTo(expected_frame_);
- ASSERT(cgen_->HasValidEntryRegisters());
- __ jmp(&label_);
- cgen_->SetFrame(current_frame);
- delete working_frame;
- __ bind(&original_fall_through);
- }
+ // We negate the condition and emit the code to merge to the expected
+ // frame immediately.
+ //
+ // TODO(): This should be replaced with a solution that emits the
+ // merge code for forward CFG edges at the appropriate entry to the
+ // target block.
+ Label original_fall_through;
+ __ j(NegateCondition(cc), &original_fall_through, NegateHint(hint));
+ VirtualFrame* working_frame = new VirtualFrame(current_frame);
+ cgen_->SetFrame(working_frame);
+ working_frame->MergeTo(expected_frame_);
+ ASSERT(cgen_->HasValidEntryRegisters());
+ __ jmp(&label_);
+ cgen_->SetFrame(current_frame);
+ delete working_frame;
+ __ bind(&original_fall_through);
}
// Postcondition: there is both a current frame and an expected frame at
// the label and they match.
@@ -215,7 +196,6 @@
// at the label.
ASSERT(cgen_ != NULL);
ASSERT(masm_ != NULL);
- ASSERT(!cgen_->IsActualFunctionReturn(this));
VirtualFrame* current_frame = cgen_->frame();
ASSERT(current_frame != NULL);
@@ -250,10 +230,6 @@
ASSERT(cgen_->HasValidEntryRegisters());
// When a label is bound the current frame becomes the expected frame
at
// the label. This requires the current frame to be mergable.
- // The frame at the actual function return will always have height
zero.
- if (cgen_->IsActualFunctionReturn(this)) {
- current_frame->Forget(current_frame->height());
- }
current_frame->MakeMergable();
ASSERT(cgen_->HasValidEntryRegisters());
expected_frame_ = new VirtualFrame(current_frame);
@@ -262,11 +238,7 @@
ASSERT(cgen_->HasValidEntryRegisters());
} else {
ASSERT(cgen_->HasValidEntryRegisters());
- // No code needs to be emitted to merge to the expected frame at the
- // actual function return.
- if (!cgen_->IsActualFunctionReturn(this)) {
- current_frame->MergeTo(expected_frame_);
- }
+ current_frame->MergeTo(expected_frame_);
ASSERT(cgen_->HasValidEntryRegisters());
}
Modified: branches/experimental/toiger/src/virtual-frame-ia32.cc
==============================================================================
--- branches/experimental/toiger/src/virtual-frame-ia32.cc (original)
+++ branches/experimental/toiger/src/virtual-frame-ia32.cc Wed Jan 7
03:35:09 2009
@@ -594,11 +594,13 @@
// copy is stored in the frame. The external reference to esi
// remains in addition to the cached copy in the frame.
Push(esi);
+ SyncElementAt(elements_.length() - 1);
- // Store the function in the frame. The frame owns the register
reference
- // now (ie, it can keep it in edi or spill it later).
+ // Store the function in the frame. The frame owns the register
+ // reference now (ie, it can keep it in edi or spill it later).
Push(edi);
cgen_->allocator()->Unuse(edi);
+ SpillElementAt(elements_.length() - 1);
}
@@ -623,6 +625,21 @@
frame_pointer_ = kIllegalIndex;
EmitPop(ebp);
+}
+
+
+void VirtualFrame::PrepareForReturn() {
+ // Spill all locals. This is necessary to make sure all locals have
+ // the right value when breaking at the return site in the debugger.
+ for (int i = 0; i < expression_base_index(); i++) SpillElementAt(i);
+
+ // Drop all non-local stack elements.
+ Drop(height());
+
+ // Validate state: The expression stack should be empty and the
+ // stack pointer should have been updated to reflect this.
+ ASSERT(height() == 0);
+ ASSERT(stack_pointer_ == expression_base_index() - 1);
}
Modified: branches/experimental/toiger/src/virtual-frame-ia32.h
==============================================================================
--- branches/experimental/toiger/src/virtual-frame-ia32.h (original)
+++ branches/experimental/toiger/src/virtual-frame-ia32.h Wed Jan 7
03:35:09 2009
@@ -237,6 +237,12 @@
void Enter();
void Exit();
+ // Prepare for returning from the frame by spilling locals and
+ // dropping all non-locals elements in the virtual frame. This
+ // avoids generating unnecessary merge code when jumping to the
+ // shared return site. Emits code for spills.
+ void PrepareForReturn();
+
// Allocate and initialize the frame-allocated locals. The eax register
// us clobbered.
void AllocateStackSlots(int count);
@@ -495,7 +501,7 @@
// Move frame elements currently in registers or constants, that
// should be in memory in the expected frame, to memory.
- void MergeMoveRegistersToMemory(VirtualFrame *expected);
+ void MergeMoveRegistersToMemory(VirtualFrame* expected);
// Make the register-to-register moves necessary to
// merge this frame with the expected frame.
@@ -504,14 +510,14 @@
// This is because some new memory-to-register moves are
// created in order to break cycles of register moves.
// Used in the implementation of MergeTo().
- void MergeMoveRegistersToRegisters(VirtualFrame *expected);
+ void MergeMoveRegistersToRegisters(VirtualFrame* expected);
// Make the memory-to-register and constant-to-register moves
// needed to make this frame equal the expected frame.
// Called after all register-to-memory and register-to-register
// moves have been made. After this function returns, the frames
// should be equal.
- void MergeMoveMemoryToRegisters(VirtualFrame *expected);
+ void MergeMoveMemoryToRegisters(VirtualFrame* expected);
};
Modified: branches/experimental/toiger/test/mjsunit/debug-evaluate.js
==============================================================================
--- branches/experimental/toiger/test/mjsunit/debug-evaluate.js (original)
+++ branches/experimental/toiger/test/mjsunit/debug-evaluate.js Wed Jan 7
03:35:09 2009
@@ -64,33 +64,33 @@
function listener(event, exec_state, event_data, data) {
try {
- if (event == Debug.DebugEvent.Break) {
- // Get the debug command processor.
- var dcp = exec_state.debugCommandProcessor();
+ if (event == Debug.DebugEvent.Break) {
+ // Get the debug command processor.
+ var dcp = exec_state.debugCommandProcessor();
- // Test some illegal evaluate requests.
- testRequest(dcp, void 0, false);
- testRequest(dcp, '{"expression":"1","global"=true}', false);
- testRequest(dcp, '{"expression":"a","frame":4}', false);
+ // Test some illegal evaluate requests.
+ testRequest(dcp, void 0, false);
+ testRequest(dcp, '{"expression":"1","global"=true}', false);
+ testRequest(dcp, '{"expression":"a","frame":4}', false);
- // Test some legal evaluate requests.
- testRequest(dcp, '{"expression":"1+2"}', true, 3);
- testRequest(dcp, '{"expression":"a+2"}', true, 5);
- testRequest(dcp, '{"expression":"({\\"a\\":1,\\"b\\":2}).b+2"}', true,
4);
+ // Test some legal evaluate requests.
+ testRequest(dcp, '{"expression":"1+2"}', true, 3);
+ testRequest(dcp, '{"expression":"a+2"}', true, 5);
+ testRequest(dcp, '{"expression":"({\\"a\\":1,\\"b\\":2}).b+2"}',
true, 4);
- // Test evaluation of a in the stack frames and the global context.
- testRequest(dcp, '{"expression":"a"}', true, 3);
- testRequest(dcp, '{"expression":"a","frame":0}', true, 3);
- testRequest(dcp, '{"expression":"a","frame":1}', true, 2);
- testRequest(dcp, '{"expression":"a","frame":2}', true, 1);
- testRequest(dcp, '{"expression":"a","global":true}', true, 1);
- testRequest(dcp, '{"expression":"this.a","global":true}', true, 1);
+ // Test evaluation of a in the stack frames and the global context.
+ testRequest(dcp, '{"expression":"a"}', true, 3);
+ testRequest(dcp, '{"expression":"a","frame":0}', true, 3);
+ testRequest(dcp, '{"expression":"a","frame":1}', true, 2);
+ testRequest(dcp, '{"expression":"a","frame":2}', true, 1);
+ testRequest(dcp, '{"expression":"a","global":true}', true, 1);
+ testRequest(dcp, '{"expression":"this.a","global":true}', true, 1);
- // Indicate that all was processed.
- listenerComplete = true;
- }
+ // Indicate that all was processed.
+ listenerComplete = true;
+ }
} catch (e) {
- exception = e
+ exception = e
};
};
Modified: branches/experimental/toiger/test/mjsunit/debug-step.js
==============================================================================
--- branches/experimental/toiger/test/mjsunit/debug-step.js (original)
+++ branches/experimental/toiger/test/mjsunit/debug-step.js Wed Jan 7
03:35:09 2009
@@ -36,8 +36,7 @@
var bp1, bp2;
function listener(event, exec_state, event_data, data) {
- if (event == Debug.DebugEvent.Break)
- {
+ if (event == Debug.DebugEvent.Break) {
if (state == 0) {
exec_state.prepareStep(Debug.StepAction.StepIn, 1000);
state = 1;
@@ -68,7 +67,6 @@
state = 0;
result = -1;
f();
-print(state);
assertEquals(499, result);
// Check that performing 1000 steps with a break point on the statement in
the
Added: branches/experimental/toiger/test/mjsunit/multiple-return.js
==============================================================================
--- (empty file)
+++ branches/experimental/toiger/test/mjsunit/multiple-return.js Wed Jan
7
03:35:09 2009
@@ -0,0 +1,62 @@
+// Copyright 2009 the V8 project authors. All rights reserved.
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following
+// disclaimer in the documentation and/or other materials provided
+// with the distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived
+// from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+function F() {
+ for (var x in [1,2,3]) {
+ return 42;
+ }
+ return 87;
+}
+
+
+function G() {
+ for (var x in [1,2,3]) {
+ try {
+ return 42;
+ } finally {
+ // Do nothing.
+ }
+ }
+ return 87;
+}
+
+
+function H() {
+ for (var x in [1,2,3]) {
+ try {
+ return 42;
+ } catch (e) {
+ // Do nothing.
+ }
+ }
+ return 87;
+}
+
+
+assertEquals(42, F());
+assertEquals(42, G());
+assertEquals(42, H());
--~--~---------~--~----~------------~-------~--~----~
v8-dev mailing list
[email protected]
http://groups.google.com/group/v8-dev
-~----------~----~----~----~------~----~------~--~---