Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jruby/jruby
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 2cdb4af7a2f5
Choose a base ref
...
head repository: jruby/jruby
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 771fe3f2a8e9
Choose a head ref
  • 3 commits
  • 8 files changed
  • 1 contributor

Commits on Oct 1, 2015

  1. Revert csv.rb to stock.

    headius committed Oct 1, 2015
    Copy the full SHA
    b228d42 View commit details
  2. Modify blocks to allow jitting like methods.

    Lots of duplicated code here between jitting methods and jitting
    blocks but the logic is largely the same. MixedModeIRBlockBody,
    like the method equivalent, increments a counter until it reaches
    JIT threshold. It then forces the block to JIT and binds it into
    a CompiledIRBlockBody, which it uses from then on to execute the
    block.
    
    Things to do:
    
    * Clean up duplicated code paths and unify more JIT stuff for both
      blocks and methods.
    * Reduce indirection through MixedModeIRBlockBody.
    * More testing of various forms of blocks.
    headius committed Oct 1, 2015
    Copy the full SHA
    318c853 View commit details
  3. Synchronize submission of code to JIT.

    The old logic could trigger a method or block to JIT twice if two
    or more threads all got to that point at the same time. For
    methods, this just resulted in wasted work. For blocks, this
    appeared to cause some classloading and/or method handle lookup
    problems that manifested as NoSuchMethod errors and stack
    overflows in the compiled code, likely due to looking up the
    wrong method name in the wrong class.
    
    The synchronization probably slows down interpretation, since it
    fires for every call when JIT is enabled, so we will want to
    change to a lock-free mechanism. This is ok for now, though,
    since with JIT enabled both blocks and methods will eventually
    stop counting calls and go straight to the jitted body.
    headius committed Oct 1, 2015
    Copy the full SHA
    771fe3f View commit details
2 changes: 1 addition & 1 deletion core/src/main/java/org/jruby/ast/DVarNode.java
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@
/**
* Access a dynamic variable (e.g. block scope local variable).
*/
public class DVarNode extends Node implements INameNode, IScopedNode {
public class DVarNode extends Node implements INameNode, IScopedNode, SideEffectFree {
// The name of the variable
private String name;

167 changes: 161 additions & 6 deletions core/src/main/java/org/jruby/compiler/JITCompiler.java
Original file line number Diff line number Diff line change
@@ -37,9 +37,12 @@
import org.jruby.ast.util.SexpMaker;
import org.jruby.internal.runtime.methods.CompiledIRMethod;
import org.jruby.internal.runtime.methods.MixedModeIRMethod;
import org.jruby.ir.IRClosure;
import org.jruby.ir.interpreter.InterpreterContext;
import org.jruby.ir.targets.JVMVisitor;
import org.jruby.ir.targets.JVMVisitorMethodContext;
import org.jruby.runtime.CompiledIRBlockBody;
import org.jruby.runtime.MixedModeIRBlockBody;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.threading.DaemonThreadFactory;
@@ -54,6 +57,7 @@
import java.lang.invoke.MethodType;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
@@ -147,7 +151,9 @@ public void tearDown() {

public Runnable getTaskFor(ThreadContext context, Compilable method) {
if (method instanceof MixedModeIRMethod) {
return new JITTask((MixedModeIRMethod) method, method.getClassName(context));
return new MethodJITTask((MixedModeIRMethod) method, method.getClassName(context));
} else if (method instanceof MixedModeIRBlockBody) {
return new BlockJITTask((MixedModeIRBlockBody) method, method.getClassName(context));
}

return new FullBuildTask(method);
@@ -204,12 +210,12 @@ public void run() {
}
}

private class JITTask implements Runnable {
private class MethodJITTask implements Runnable {
private final String className;
private final MixedModeIRMethod method;
private final String methodName;

public JITTask(MixedModeIRMethod method, String className) {
public MethodJITTask(MixedModeIRMethod method, String className) {
this.method = method;
this.className = className;
this.methodName = method.getName();
@@ -241,7 +247,7 @@ public void run() {

String key = SexpMaker.sha1(method.getIRScope());
JVMVisitor visitor = new JVMVisitor();
JITClassGenerator generator = new JITClassGenerator(className, methodName, key, runtime, method, visitor);
MethodJITClassGenerator generator = new MethodJITClassGenerator(className, methodName, key, runtime, method, visitor);

JVMVisitorMethodContext context = new JVMVisitorMethodContext();
generator.compile(context);
@@ -317,6 +323,68 @@ public void run() {
}
}

private class BlockJITTask implements Runnable {
private final String className;
private final MixedModeIRBlockBody body;
private final String methodName;

public BlockJITTask(MixedModeIRBlockBody body, String className) {
this.body = body;
this.className = className;
this.methodName = body.getName();
}

public void run() {
try {
String key = SexpMaker.sha1(body.getIRScope());
JVMVisitor visitor = new JVMVisitor();
BlockJITClassGenerator generator = new BlockJITClassGenerator(className, methodName, key, runtime, body, visitor);

JVMVisitorMethodContext context = new JVMVisitorMethodContext();
generator.compile(context);

// FIXME: reinstate active bytecode size check
// At this point we still need to reinstate the bytecode size check, to ensure we're not loading code
// that's so big that JVMs won't even try to compile it. Removed the check because with the new IR JIT
// bytecode counts often include all nested scopes, even if they'd be different methods. We need a new
// mechanism of getting all body sizes.
Class sourceClass = visitor.defineFromBytecode(body.getIRScope(), generator.bytecode(), new OneShotClassLoader(runtime.getJRubyClassLoader()));

if (sourceClass == null) {
// class could not be found nor generated; give up on JIT and bail out
counts.failCount.incrementAndGet();
return;
} else {
generator.updateCounters(counts);
}

// successfully got back a jitted body

if (config.isJitLogging()) {
log(body.getImplementationClass(), body.getFile(), body.getLine(), className + "." + methodName, "done jitting");
}

String jittedName = context.getJittedName();

// blocks only have variable-arity
body.completeBuild(
new CompiledIRBlockBody(
PUBLIC_LOOKUP.findStatic(sourceClass, jittedName, JVMVisitor.CLOSURE_SIGNATURE.type()),
body.getIRScope(),
((IRClosure) body.getIRScope()).getSignature().encode()));
} catch (Throwable t) {
if (config.isJitLogging()) {
log(body.getImplementationClass(), body.getFile(), body.getLine(), className + "." + methodName, "Could not compile; passes run: " + body.getIRScope().getExecutedPasses(), t.getMessage());
if (config.isJitLoggingVerbose()) {
t.printStackTrace();
}
}

counts.failCount.incrementAndGet();
}
}
}

public static String getHashForString(String str) {
return getHashForBytes(RubyEncoding.encodeUTF8(str));
}
@@ -335,8 +403,8 @@ public static String getHashForBytes(byte[] bytes) {
}
}

public static class JITClassGenerator {
public JITClassGenerator(String className, String methodName, String key, Ruby ruby, MixedModeIRMethod method, JVMVisitor visitor) {
public static class MethodJITClassGenerator {
public MethodJITClassGenerator(String className, String methodName, String key, Ruby ruby, MixedModeIRMethod method, JVMVisitor visitor) {
this.packageName = JITCompiler.RUBY_JIT_PREFIX;
if (RubyInstanceConfig.JAVA_VERSION == Opcodes.V1_7 || Options.COMPILE_INVOKEDYNAMIC.load() == true) {
// Some versions of Java 7 seems to have a bug that leaks definitions across cousin classloaders
@@ -422,6 +490,93 @@ public String toString() {
private String name;
}

public static class BlockJITClassGenerator {
public BlockJITClassGenerator(String className, String methodName, String key, Ruby ruby, MixedModeIRBlockBody body, JVMVisitor visitor) {
this.packageName = JITCompiler.RUBY_JIT_PREFIX;
if (RubyInstanceConfig.JAVA_VERSION == Opcodes.V1_7 || Options.COMPILE_INVOKEDYNAMIC.load() == true) {
// Some versions of Java 7 seems to have a bug that leaks definitions across cousin classloaders
// so we force the class name to be unique to this runtime.

// Also, invokedynamic forces us to make jitted bytecode unique to each runtime, since the call sites cache
// at class level rather than at our runtime level. This makes it impossible to share jitted bytecode
// across runtimes.

digestString = key + Math.abs(ruby.hashCode());
} else {
digestString = key;
}
this.className = packageName + "/" + className.replace('.', '/') + CLASS_METHOD_DELIMITER + JavaNameMangler.mangleMethodName(methodName) + "_" + digestString;
this.name = this.className.replaceAll("/", ".");
this.methodName = methodName;
this.body = body;
this.visitor = visitor;
}

@SuppressWarnings("unchecked")
protected void compile(JVMVisitorMethodContext context) {
if (bytecode != null) return;

// Time the compilation
long start = System.nanoTime();

InterpreterContext ic = body.ensureInstrsReady();

int insnCount = ic.getInstructions().length;
if (insnCount > Options.JIT_MAXSIZE.load()) {
// methods with more than our limit of basic blocks are likely too large to JIT, so bail out
throw new NotCompilableException("Could not compile " + body + "; instruction count " + insnCount + " exceeds threshold of " + Options.JIT_MAXSIZE.load());
}

// This may not be ok since we'll end up running passes specific to JIT
// CON FIXME: Really should clone scope before passes in any case
bytecode = visitor.compileToBytecode(body.getIRScope(), context);

compileTime = System.nanoTime() - start;
}

void updateCounters(JITCounts counts) {
counts.compiledCount.incrementAndGet();
counts.compileTime.addAndGet(compileTime);
counts.codeSize.addAndGet(bytecode.length);
counts.averageCompileTime.set(counts.compileTime.get() / counts.compiledCount.get());
counts.averageCodeSize.set(counts.codeSize.get() / counts.compiledCount.get());
synchronized (counts) {
if (counts.largestCodeSize.get() < bytecode.length) {
counts.largestCodeSize.set(bytecode.length);
}
}
}

// FIXME: Does anything call this? If so we should document it.
public void generate() {
compile(new JVMVisitorMethodContext());
}

public byte[] bytecode() {
return bytecode;
}

public String name() {
return name;
}

@Override
public String toString() {
return "{} at " + body.getFile() + ":" + body.getLine();
}

private final String packageName;
private final String className;
private final String methodName;
private final String digestString;
private final MixedModeIRBlockBody body;
private final JVMVisitor visitor;

private byte[] bytecode;
private long compileTime;
private String name;
}

static void log(RubyModule implementationClass, String file, int line, String name, String message, String... reason) {
boolean isBlock = implementationClass == null;
String className = isBlock ? "<block>" : implementationClass.getBaseName();
Original file line number Diff line number Diff line change
@@ -30,8 +30,8 @@ public class MixedModeIRMethod extends DynamicMethod implements IRMethodArgs, Po
protected final IRScope method;

protected static class DynamicMethodBox {
public DynamicMethod actualMethod;
public int callCount = 0;
public volatile DynamicMethod actualMethod;
public volatile int callCount = 0;
}

protected DynamicMethodBox box = new DynamicMethodBox();
@@ -308,7 +308,14 @@ public void completeBuild(DynamicMethod newMethod) {
protected void tryJit(ThreadContext context, DynamicMethodBox box) {
if (context.runtime.isBooting()) return; // don't JIT during runtime boot

if (box.callCount++ >= Options.JIT_THRESHOLD.load()) context.runtime.getJITCompiler().buildThresholdReached(context, this);
synchronized (this) {
if (box.callCount >= 0) {
if (box.callCount++ >= Options.JIT_THRESHOLD.load()) {
box.callCount = -1;
context.runtime.getJITCompiler().buildThresholdReached(context, this);
}
}
}
}

public String getClassName(ThreadContext context) {
8 changes: 6 additions & 2 deletions core/src/main/java/org/jruby/ir/IRClosure.java
Original file line number Diff line number Diff line change
@@ -2,17 +2,21 @@

import java.util.ArrayList;
import java.util.List;

import org.jruby.RubyInstanceConfig;
import org.jruby.ir.instructions.*;
import org.jruby.ir.interpreter.ClosureInterpreterContext;
import org.jruby.ir.interpreter.InterpreterContext;
import org.jruby.ir.operands.*;
import org.jruby.ir.representations.BasicBlock;
import org.jruby.ir.transformations.inlining.CloneInfo;
import org.jruby.ir.transformations.inlining.SimpleCloneInfo;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.ArgumentDescriptor;
import org.jruby.runtime.BlockBody;
import org.jruby.runtime.IRBlockBody;
import org.jruby.runtime.InterpretedIRBlockBody;
import org.jruby.runtime.MixedModeIRBlockBody;
import org.jruby.runtime.Signature;
import org.objectweb.asm.Handle;

@@ -60,7 +64,7 @@ protected IRClosure(IRClosure c, IRScope lexicalParent, int closureId, String fu
if (getManager().isDryRun()) {
this.body = null;
} else {
this.body = new InterpretedIRBlockBody(this, c.body.getSignature());
this.body = new MixedModeIRBlockBody(c, c.getSignature());
}

this.signature = c.signature;
@@ -82,7 +86,7 @@ public IRClosure(IRManager manager, IRScope lexicalParent, int lineNumber, Stati
if (getManager().isDryRun()) {
this.body = null;
} else {
this.body = new InterpretedIRBlockBody(this, signature);
this.body = new MixedModeIRBlockBody(this, signature);
if (staticScope != null && !isBeginEndBlock) {
staticScope.setIRScope(this);
staticScope.setScopeType(this.getScopeType());
19 changes: 17 additions & 2 deletions core/src/main/java/org/jruby/ir/targets/JVMVisitor.java
Original file line number Diff line number Diff line change
@@ -107,6 +107,8 @@ public void codegenScope(IRScope scope, JVMVisitorMethodContext context) {
codegenScriptBody((IRScriptBody)scope);
} else if (scope instanceof IRMethod) {
emitMethodJIT((IRMethod)scope, context);
} else if (scope instanceof IRClosure) {
emitBlockJIT((IRClosure) scope, context);
} else if (scope instanceof IRModuleBody) {
emitModuleBodyJIT((IRModuleBody)scope);
} else {
@@ -222,7 +224,7 @@ public static final Signature signatureFor(IRScope method, boolean aritySplit) {
return METHOD_SIGNATURE_BASE.insertArgs(3, new String[]{"args"}, IRubyObject[].class);
}

private static final Signature CLOSURE_SIGNATURE = Signature
public static final Signature CLOSURE_SIGNATURE = Signature
.returning(IRubyObject.class)
.appendArgs(new String[]{"context", "scope", "self", "args", "block", "superName", "type"}, ThreadContext.class, StaticScope.class, IRubyObject.class, IRubyObject[].class, Block.class, String.class, Block.Type.class);

@@ -244,7 +246,7 @@ public void emitMethod(IRMethod method, JVMVisitorMethodContext context) {
emitWithSignatures(method, context, name);
}

public void emitMethodJIT(IRMethod method, JVMVisitorMethodContext context) {
public void emitMethodJIT(IRMethod method, JVMVisitorMethodContext context) {
String clsName = jvm.scriptToClass(method.getFileName());
String name = JavaNameMangler.encodeScopeForBacktrace(method) + "$" + methodIndex++;
jvm.pushscript(clsName, method.getFileName());
@@ -255,6 +257,19 @@ public void emitMethodJIT(IRMethod method, JVMVisitorMethodContext context) {
jvm.popclass();
}

public void emitBlockJIT(IRClosure closure, JVMVisitorMethodContext context) {
String clsName = jvm.scriptToClass(closure.getFileName());
String name = JavaNameMangler.encodeScopeForBacktrace(closure) + "$" + methodIndex++;
jvm.pushscript(clsName, closure.getFileName());

emitScope(closure, name, CLOSURE_SIGNATURE, false);

context.setJittedName(name);

jvm.cls().visitEnd();
jvm.popclass();
}

private void emitWithSignatures(IRMethod method, JVMVisitorMethodContext context, String name) {
context.setJittedName(name);

2 changes: 1 addition & 1 deletion core/src/main/java/org/jruby/runtime/IRBlockBody.java
Original file line number Diff line number Diff line change
@@ -82,7 +82,7 @@ public IRubyObject yieldSpecific(ThreadContext context, IRubyObject arg0, Bindin
}
}

private IRubyObject yieldSpecificMultiArgsCommon(ThreadContext context, IRubyObject[] args, Binding binding, Type type) {
IRubyObject yieldSpecificMultiArgsCommon(ThreadContext context, IRubyObject[] args, Binding binding, Type type) {
int blockArity = getSignature().arityValue();
if (blockArity == 0) {
args = IRubyObject.NULL_ARRAY; // discard args
167 changes: 167 additions & 0 deletions core/src/main/java/org/jruby/runtime/MixedModeIRBlockBody.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package org.jruby.runtime;

import org.jruby.EvalType;
import org.jruby.RubyModule;
import org.jruby.compiler.Compilable;
import org.jruby.ir.IRClosure;
import org.jruby.ir.IRScope;
import org.jruby.ir.interpreter.Interpreter;
import org.jruby.ir.interpreter.InterpreterContext;
import org.jruby.ir.runtime.IRRuntimeHelpers;
import org.jruby.runtime.Block.Type;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.cli.Options;
import org.jruby.util.log.Logger;
import org.jruby.util.log.LoggerFactory;

public class MixedModeIRBlockBody extends IRBlockBody implements Compilable<CompiledIRBlockBody> {
private static final Logger LOG = LoggerFactory.getLogger("InterpretedIRBlockBody");
protected boolean pushScope;
protected boolean reuseParentScope;
private boolean displayedCFG = false; // FIXME: Remove when we find nicer way of logging CFG
private volatile int callCount = 0;
private InterpreterContext interpreterContext;
private volatile CompiledIRBlockBody jittedBody;

public MixedModeIRBlockBody(IRClosure closure, Signature signature) {
super(closure, signature);
this.pushScope = true;
this.reuseParentScope = false;

// JIT currently JITs blocks along with their method and no on-demand by themselves. We only
// promote to full build here if we are -X-C.
if (!closure.getManager().getInstanceConfig().getCompileMode().shouldJIT()) {
callCount = -1;
}
}

@Override
public void setEvalType(EvalType evalType) {
this.evalType.set(evalType);
if (jittedBody != null) jittedBody.setEvalType(evalType);
}

@Override
public void setCallCount(int callCount) {
this.callCount = callCount;
}

@Override
public void completeBuild(CompiledIRBlockBody blockBody) {
this.callCount = -1;
this.jittedBody = blockBody;
}

@Override
public IRScope getIRScope() {
return closure;
}

public BlockBody getJittedBody() {
return jittedBody;
}

@Override
public ArgumentDescriptor[] getArgumentDescriptors() {
return closure.getArgumentDescriptors();
}

public InterpreterContext ensureInstrsReady() {
if (IRRuntimeHelpers.isDebug() && !displayedCFG) {
LOG.info("Executing '" + closure + "' (pushScope=" + pushScope + ", reuseParentScope=" + reuseParentScope);
LOG.info(closure.debugOutput());
displayedCFG = true;
}

if (interpreterContext == null) {
interpreterContext = closure.getInterpreterContext();
}
return interpreterContext;
}

@Override
public String getClassName(ThreadContext context) {
return closure.getName();
}

@Override
public String getName() {
return closure.getName();
}

protected IRubyObject commonYieldPath(ThreadContext context, IRubyObject[] args, IRubyObject self, Binding binding, Type type, Block block) {
if (callCount >= 0) promoteToFullBuild(context);

CompiledIRBlockBody jittedBody = this.jittedBody;

if (jittedBody != null) {
return jittedBody.commonYieldPath(context, args, self, binding, type, block);
}

// SSS: Important! Use getStaticScope() to use a copy of the static-scope stored in the block-body.
// Do not use 'closure.getStaticScope()' -- that returns the original copy of the static scope.
// This matters because blocks created for Thread bodies modify the static-scope field of the block-body
// that records additional state about the block body.
//
// FIXME: Rather than modify static-scope, it seems we ought to set a field in block-body which is then
// used to tell dynamic-scope that it is a dynamic scope for a thread body. Anyway, to be revisited later!
Visibility oldVis = binding.getFrame().getVisibility();
Frame prevFrame = context.preYieldNoScope(binding);

// SSS FIXME: Why is self null in non-binding-eval contexts?
if (self == null || this.evalType.get() == EvalType.BINDING_EVAL) {
self = useBindingSelf(binding);
}

// SSS FIXME: Maybe, we should allocate a NoVarsScope/DummyScope for for-loop bodies because the static-scope here
// probably points to the parent scope? To be verified and fixed if necessary. There is no harm as it is now. It
// is just wasteful allocation since the scope is not used at all.

InterpreterContext ic = ensureInstrsReady();

// Pass on eval state info to the dynamic scope and clear it on the block-body
DynamicScope actualScope = binding.getDynamicScope();
if (ic.pushNewDynScope()) {
actualScope = DynamicScope.newDynamicScope(getStaticScope(), actualScope, this.evalType.get());
if (type == Type.LAMBDA) actualScope.setLambda(true);
context.pushScope(actualScope);
} else if (ic.reuseParentDynScope()) {
// Reuse! We can avoid the push only if surrounding vars aren't referenced!
context.pushScope(actualScope);
}
this.evalType.set(EvalType.NONE);

try {
return Interpreter.INTERPRET_BLOCK(context, self, ic, args, binding.getMethod(), block, type);
}
finally {
// IMPORTANT: Do not clear eval-type in case this is reused in bindings!
// Ex: eval("...", foo.instance_eval { binding })
// The dyn-scope used for binding needs to have its eval-type set to INSTANCE_EVAL
binding.getFrame().setVisibility(oldVis);
if (ic.popDynScope()) {
context.postYield(binding, prevFrame);
} else {
context.postYieldNoScope(prevFrame);
}
}
}

protected void promoteToFullBuild(ThreadContext context) {
if (context.runtime.isBooting()) return; // don't JIT during runtime boot

synchronized (this) {
if (callCount >= 0) {
if (callCount++ >= Options.JIT_THRESHOLD.load()) {
callCount = -1;
context.runtime.getJITCompiler().buildThresholdReached(context, this);
}
}
}
}

public RubyModule getImplementationClass() {
return null;
}

}
60 changes: 26 additions & 34 deletions lib/ruby/stdlib/csv.rb
Original file line number Diff line number Diff line change
@@ -944,31 +944,29 @@ class MalformedCSVError < RuntimeError; end
# To add a combo field, the value should be an Array of names. Combo fields
# can be nested with other combo fields.
#
converter_methods = Module.new do
def self.integer(f)
Integer(f.encode(ConverterEncoding)) rescue f
end
def self.float(f)
Float(f.encode(ConverterEncoding)) rescue f
end
def self.date(f)
begin
e = f.encode(ConverterEncoding)
e =~ DateMatcher ? Date.parse(e) : f
end rescue r # encoding conversion or date parse errors
end
def self.date_time(f)
begin
e = f.encode(ConverterEncoding)
e =~ DateTimeMatcher ? DateTime.parse(e) : f
end rescue r # encoding conversion or date parse errors
end
end
Converters = { integer: converter_methods.method(:integer),
float: converter_methods.method(:float),
Converters = { integer: lambda { |f|
Integer(f.encode(ConverterEncoding)) rescue f
},
float: lambda { |f|
Float(f.encode(ConverterEncoding)) rescue f
},
numeric: [:integer, :float],
date: converter_methods.method(:date),
date_time: converter_methods.method(:date_time),
date: lambda { |f|
begin
e = f.encode(ConverterEncoding)
e =~ DateMatcher ? Date.parse(e) : f
rescue # encoding conversion or date parse errors
f
end
},
date_time: lambda { |f|
begin
e = f.encode(ConverterEncoding)
e =~ DateTimeMatcher ? DateTime.parse(e) : f
rescue # encoding conversion or date parse errors
f
end
},
all: [:date_time, :numeric] }

#
@@ -991,18 +989,12 @@ def self.date_time(f)
# To add a combo field, the value should be an Array of names. Combo fields
# can be nested with other combo fields.
#
header_converters = Module.new do
def self.downcase(h)
h.encode(ConverterEncoding).downcase
end
def self.symbol(h)
HeaderConverters = {
downcase: lambda { |h| h.encode(ConverterEncoding).downcase },
symbol: lambda { |h|
h.encode(ConverterEncoding).downcase.strip.gsub(/\s+/, "_").
gsub(/\W+/, "").to_sym
end
end
HeaderConverters = {
downcase: header_converters.method(:downcase),
symbol: header_converters.method(:symbol)
}
}

#