Line data Source code
1 : /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 : /*
3 : * This file is part of the LibreOffice project.
4 : *
5 : * This Source Code Form is subject to the terms of the Mozilla Public
6 : * License, v. 2.0. If a copy of the MPL was not distributed with this
7 : * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 : */
9 :
10 : #include <config_features.h>
11 :
12 : #include "formulagroup.hxx"
13 : #include "formulagroupcl.hxx"
14 : #include "document.hxx"
15 : #include "formulacell.hxx"
16 : #include "tokenarray.hxx"
17 : #include "compiler.hxx"
18 : #include "interpre.hxx"
19 : #include "scmatrix.hxx"
20 : #include "globalnames.hxx"
21 :
22 : #include <formula/vectortoken.hxx>
23 : #include <rtl/bootstrap.hxx>
24 :
25 : #include <vector>
26 : #include <boost/scoped_array.hpp>
27 : #include <boost/unordered_map.hpp>
28 :
29 : #define USE_DUMMY_INTERPRETER 0
30 :
31 : #include <cstdio>
32 :
33 : #if HAVE_FEATURE_OPENCL
34 :
35 : #include "openclwrapper.hxx"
36 :
37 : #endif
38 :
39 : namespace sc {
40 :
41 114 : FormulaGroupEntry::FormulaGroupEntry( ScFormulaCell** pCells, size_t nRow, size_t nLength ) :
42 114 : mpCells(pCells), mnRow(nRow), mnLength(nLength), mbShared(true) {}
43 :
44 442 : FormulaGroupEntry::FormulaGroupEntry( ScFormulaCell* pCell, size_t nRow ) :
45 442 : mpCell(pCell), mnRow(nRow), mnLength(0), mbShared(false) {}
46 :
47 90 : size_t FormulaGroupContext::ColKey::Hash::operator ()( const FormulaGroupContext::ColKey& rKey ) const
48 : {
49 90 : return rKey.mnTab * MAXCOLCOUNT + rKey.mnCol;
50 : }
51 :
52 90 : FormulaGroupContext::ColKey::ColKey( SCTAB nTab, SCCOL nCol ) : mnTab(nTab), mnCol(nCol) {}
53 :
54 28 : bool FormulaGroupContext::ColKey::operator== ( const ColKey& r ) const
55 : {
56 28 : return mnTab == r.mnTab && mnCol == r.mnCol;
57 : }
58 :
59 0 : bool FormulaGroupContext::ColKey::operator!= ( const ColKey& r ) const
60 : {
61 0 : return !operator==(r);
62 : }
63 :
64 30 : FormulaGroupContext::ColArray::ColArray( NumArrayType* pNumArray, StrArrayType* pStrArray ) :
65 30 : mpNumArray(pNumArray), mpStrArray(pStrArray), mnSize(0)
66 : {
67 30 : if (mpNumArray)
68 22 : mnSize = mpNumArray->size();
69 8 : else if (mpStrArray)
70 8 : mnSize = mpStrArray->size();
71 30 : }
72 :
73 40 : FormulaGroupContext::ColArray* FormulaGroupContext::getCachedColArray( SCTAB nTab, SCCOL nCol, size_t nSize )
74 : {
75 40 : ColArraysType::iterator itColArray = maColArrays.find(ColKey(nTab, nCol));
76 40 : if (itColArray == maColArrays.end())
77 : // Not cached for this column.
78 22 : return NULL;
79 :
80 18 : ColArray& rCached = itColArray->second;
81 18 : if (nSize > rCached.mnSize)
82 : // Cached data array is not long enough for the requested range.
83 10 : return NULL;
84 :
85 8 : return &rCached;
86 : }
87 :
88 30 : FormulaGroupContext::ColArray* FormulaGroupContext::setCachedColArray(
89 : SCTAB nTab, SCCOL nCol, NumArrayType* pNumArray, StrArrayType* pStrArray )
90 : {
91 30 : ColArraysType::iterator it = maColArrays.find(ColKey(nTab, nCol));
92 30 : if (it == maColArrays.end())
93 : {
94 : std::pair<ColArraysType::iterator,bool> r =
95 : maColArrays.insert(
96 20 : ColArraysType::value_type(ColKey(nTab, nCol), ColArray(pNumArray, pStrArray)));
97 :
98 20 : if (!r.second)
99 : // Somehow the insertion failed.
100 0 : return NULL;
101 :
102 20 : return &r.first->second;
103 : }
104 :
105 : // Prior array exists for this column. Overwrite it.
106 10 : ColArray& rArray = it->second;
107 10 : rArray = ColArray(pNumArray, pStrArray);
108 10 : return &rArray;
109 : }
110 :
111 8 : void FormulaGroupContext::ensureStrArray( ColArray& rColArray, size_t nArrayLen )
112 : {
113 8 : if (rColArray.mpStrArray)
114 10 : return;
115 :
116 : maStrArrays.push_back(
117 6 : new sc::FormulaGroupContext::StrArrayType(nArrayLen, NULL));
118 6 : rColArray.mpStrArray = &maStrArrays.back();
119 : }
120 :
121 20 : void FormulaGroupContext::ensureNumArray( ColArray& rColArray, size_t nArrayLen )
122 : {
123 20 : if (rColArray.mpNumArray)
124 38 : return;
125 :
126 : double fNan;
127 2 : rtl::math::setNan(&fNan);
128 :
129 : maNumArrays.push_back(
130 2 : new sc::FormulaGroupContext::NumArrayType(nArrayLen, fNan));
131 2 : rColArray.mpNumArray = &maNumArrays.back();
132 : }
133 :
134 14 : FormulaGroupContext::FormulaGroupContext()
135 : {
136 14 : }
137 :
138 14 : FormulaGroupContext::~FormulaGroupContext()
139 : {
140 14 : }
141 :
142 : namespace {
143 :
144 : /**
145 : * Input double array consists of segments of NaN's and normal values.
146 : * Insert only the normal values into the matrix while skipping the NaN's.
147 : */
148 0 : void fillMatrix( ScMatrix& rMat, size_t nCol, const double* pNums, size_t nLen )
149 : {
150 0 : const double* pNum = pNums;
151 0 : const double* pNumEnd = pNum + nLen;
152 0 : const double* pNumHead = NULL;
153 0 : for (; pNum != pNumEnd; ++pNum)
154 : {
155 0 : if (!rtl::math::isNan(*pNum))
156 : {
157 0 : if (!pNumHead)
158 : // Store the first non-NaN position.
159 0 : pNumHead = pNum;
160 :
161 0 : continue;
162 : }
163 :
164 0 : if (pNumHead)
165 : {
166 : // Flush this non-NaN segment to the matrix.
167 0 : rMat.PutDouble(pNumHead, pNum - pNumHead, nCol, pNumHead - pNums);
168 0 : pNumHead = NULL;
169 : }
170 : }
171 :
172 0 : if (pNumHead)
173 : {
174 : // Flush last non-NaN segment to the matrix.
175 0 : rMat.PutDouble(pNumHead, pNum - pNumHead, nCol, pNumHead - pNums);
176 : }
177 0 : }
178 :
179 0 : void flushStrSegment(
180 : ScMatrix& rMat, size_t nCol, rtl_uString** pHead, rtl_uString** pCur, rtl_uString** pTop )
181 : {
182 0 : size_t nOffset = pHead - pTop;
183 0 : std::vector<svl::SharedString> aStrs;
184 0 : aStrs.reserve(pCur - pHead);
185 0 : for (; pHead != pCur; ++pHead)
186 0 : aStrs.push_back(svl::SharedString(*pHead, *pHead));
187 :
188 0 : rMat.PutString(&aStrs[0], aStrs.size(), nCol, nOffset);
189 0 : }
190 :
191 0 : void fillMatrix( ScMatrix& rMat, size_t nCol, rtl_uString** pStrs, size_t nLen )
192 : {
193 0 : rtl_uString** p = pStrs;
194 0 : rtl_uString** pEnd = p + nLen;
195 0 : rtl_uString** pHead = NULL;
196 0 : for (; p != pEnd; ++p)
197 : {
198 0 : if (*p)
199 : {
200 0 : if (!pHead)
201 : // Store the first non-empty string position.
202 0 : pHead = p;
203 :
204 0 : continue;
205 : }
206 :
207 0 : if (pHead)
208 : {
209 : // Flush this non-empty segment to the matrix.
210 0 : flushStrSegment(rMat, nCol, pHead, p, pStrs);
211 0 : pHead = NULL;
212 : }
213 : }
214 :
215 0 : if (pHead)
216 : {
217 : // Flush last non-empty segment to the matrix.
218 0 : flushStrSegment(rMat, nCol, pHead, p, pStrs);
219 : }
220 0 : }
221 :
222 0 : void fillMatrix( ScMatrix& rMat, size_t nCol, const double* pNums, rtl_uString** pStrs, size_t nLen )
223 : {
224 0 : if (!pStrs)
225 : {
226 0 : fillMatrix(rMat, nCol, pNums, nLen);
227 0 : return;
228 : }
229 :
230 0 : const double* pNum = pNums;
231 0 : const double* pNumHead = NULL;
232 0 : rtl_uString** pStr = pStrs;
233 0 : rtl_uString** pStrEnd = pStr + nLen;
234 0 : rtl_uString** pStrHead = NULL;
235 :
236 0 : for (; pStr != pStrEnd; ++pStr, ++pNum)
237 : {
238 0 : if (*pStr)
239 : {
240 : // String cell exists.
241 :
242 0 : if (pNumHead)
243 : {
244 : // Flush this numeric segment to the matrix.
245 0 : rMat.PutDouble(pNumHead, pNum - pNumHead, nCol, pNumHead - pNums);
246 0 : pNumHead = NULL;
247 : }
248 :
249 0 : if (!pStrHead)
250 : // Store the first non-empty string position.
251 0 : pStrHead = pStr;
252 :
253 0 : continue;
254 : }
255 :
256 : // No string cell. Check the numeric cell value.
257 :
258 0 : if (pStrHead)
259 : {
260 : // Flush this non-empty string segment to the matrix.
261 0 : flushStrSegment(rMat, nCol, pStrHead, pStr, pStrs);
262 0 : pStrHead = NULL;
263 : }
264 :
265 0 : if (!rtl::math::isNan(*pNum))
266 : {
267 : // Numeric cell exists.
268 0 : if (!pNumHead)
269 : // Store the first non-NaN position.
270 0 : pNumHead = pNum;
271 :
272 0 : continue;
273 : }
274 :
275 : // Empty cell. No action required.
276 : }
277 :
278 0 : if (pStrHead)
279 : {
280 : // Flush the last non-empty segment to the matrix.
281 0 : flushStrSegment(rMat, nCol, pStrHead, pStr, pStrs);
282 : }
283 0 : else if (pNumHead)
284 : {
285 : // Flush the last numeric segment to the matrix.
286 0 : rMat.PutDouble(pNumHead, pNum - pNumHead, nCol, pNumHead - pNums);
287 : }
288 : }
289 :
290 : }
291 :
292 0 : CompiledFormula::CompiledFormula() {}
293 :
294 0 : CompiledFormula::~CompiledFormula() {}
295 :
296 0 : FormulaGroupInterpreterSoftware::FormulaGroupInterpreterSoftware() : FormulaGroupInterpreter()
297 : {
298 0 : }
299 :
300 0 : ScMatrixRef FormulaGroupInterpreterSoftware::inverseMatrix(const ScMatrix& /*rMat*/)
301 : {
302 0 : return ScMatrixRef();
303 : }
304 :
305 0 : CompiledFormula* FormulaGroupInterpreterSoftware::createCompiledFormula(ScDocument& /* rDoc */,
306 : const ScAddress& /* rTopPos */,
307 : ScFormulaCellGroup& /* rGroup */,
308 : ScTokenArray& /* rCode */)
309 : {
310 0 : return NULL;
311 : }
312 :
313 0 : bool FormulaGroupInterpreterSoftware::interpret(ScDocument& rDoc, const ScAddress& rTopPos,
314 : ScFormulaCellGroupRef& xGroup,
315 : ScTokenArray& rCode)
316 : {
317 : typedef boost::unordered_map<const formula::FormulaToken*, formula::FormulaTokenRef> CachedTokensType;
318 :
319 : // Decompose the group into individual cells and calculate them individually.
320 :
321 : // The caller must ensure that the top position is the start position of
322 : // the group.
323 :
324 0 : ScAddress aTmpPos = rTopPos;
325 0 : std::vector<formula::FormulaTokenRef> aResults;
326 0 : aResults.reserve(xGroup->mnLength);
327 0 : CachedTokensType aCachedTokens;
328 :
329 : double fNan;
330 0 : rtl::math::setNan(&fNan);
331 :
332 0 : for (SCROW i = 0; i < xGroup->mnLength; ++i, aTmpPos.IncRow())
333 : {
334 0 : ScTokenArray aCode2;
335 0 : for (const formula::FormulaToken* p = rCode.First(); p; p = rCode.Next())
336 : {
337 0 : CachedTokensType::iterator it = aCachedTokens.find(p);
338 0 : if (it != aCachedTokens.end())
339 : {
340 : // This token is cached. Use the cached one.
341 0 : aCode2.AddToken(*it->second);
342 0 : continue;
343 : }
344 :
345 0 : switch (p->GetType())
346 : {
347 : case formula::svSingleVectorRef:
348 : {
349 0 : const formula::SingleVectorRefToken* p2 = static_cast<const formula::SingleVectorRefToken*>(p);
350 0 : const formula::VectorRefArray& rArray = p2->GetArray();
351 :
352 0 : rtl_uString* pStr = NULL;
353 0 : double fVal = fNan;
354 0 : if (static_cast<size_t>(i) < p2->GetArrayLength())
355 : {
356 0 : if (rArray.mpStringArray)
357 : // See if the cell is of string type.
358 0 : pStr = rArray.mpStringArray[i];
359 :
360 0 : if (!pStr && rArray.mpNumericArray)
361 0 : fVal = rArray.mpNumericArray[i];
362 : }
363 :
364 0 : if (pStr)
365 : {
366 : // This is a string cell.
367 0 : svl::SharedStringPool& rPool = rDoc.GetSharedStringPool();
368 0 : aCode2.AddString(rPool.intern(OUString(pStr)));
369 : }
370 0 : else if (rtl::math::isNan(fVal))
371 : // Value of NaN represents an empty cell.
372 0 : aCode2.AddToken(ScEmptyCellToken(false, false));
373 : else
374 : // Numeric cell.
375 0 : aCode2.AddDouble(fVal);
376 : }
377 0 : break;
378 : case formula::svDoubleVectorRef:
379 : {
380 0 : const formula::DoubleVectorRefToken* p2 = static_cast<const formula::DoubleVectorRefToken*>(p);
381 0 : const std::vector<formula::VectorRefArray>& rArrays = p2->GetArrays();
382 0 : size_t nColSize = rArrays.size();
383 0 : size_t nRowStart = p2->IsStartFixed() ? 0 : i;
384 0 : size_t nRowEnd = p2->GetRefRowSize() - 1;
385 0 : if (!p2->IsEndFixed())
386 0 : nRowEnd += i;
387 0 : size_t nRowSize = nRowEnd - nRowStart + 1;
388 0 : ScMatrixRef pMat(new ScMatrix(nColSize, nRowSize));
389 :
390 0 : size_t nDataRowEnd = p2->GetArrayLength() - 1;
391 0 : if (nRowStart > nDataRowEnd)
392 : // Referenced rows are all empty.
393 0 : nRowSize = 0;
394 0 : else if (nRowEnd > nDataRowEnd)
395 : // Data array is shorter than the row size of the reference. Truncate it to the data.
396 0 : nRowSize -= nRowEnd - nDataRowEnd;
397 :
398 0 : for (size_t nCol = 0; nCol < nColSize; ++nCol)
399 : {
400 0 : const formula::VectorRefArray& rArray = rArrays[nCol];
401 0 : if (rArray.mpStringArray)
402 : {
403 0 : if (rArray.mpNumericArray)
404 : {
405 : // Mixture of string and numeric values.
406 0 : const double* pNums = rArray.mpNumericArray;
407 0 : pNums += nRowStart;
408 0 : rtl_uString** pStrs = rArray.mpStringArray;
409 0 : pStrs += nRowStart;
410 0 : fillMatrix(*pMat, nCol, pNums, pStrs, nRowSize);
411 : }
412 : else
413 : {
414 : // String cells only.
415 0 : rtl_uString** pStrs = rArray.mpStringArray;
416 0 : pStrs += nRowStart;
417 0 : fillMatrix(*pMat, nCol, pStrs, nRowSize);
418 : }
419 : }
420 0 : else if (rArray.mpNumericArray)
421 : {
422 : // Numeric cells only.
423 0 : const double* pNums = rArray.mpNumericArray;
424 0 : pNums += nRowStart;
425 0 : fillMatrix(*pMat, nCol, pNums, nRowSize);
426 : }
427 : }
428 :
429 0 : if (p2->IsStartFixed() && p2->IsEndFixed())
430 : {
431 : // Cached the converted token for absolute range referene.
432 : ScComplexRefData aRef;
433 0 : ScRange aRefRange = rTopPos;
434 0 : aRefRange.aEnd.SetRow(rTopPos.Row() + nRowEnd);
435 0 : aRef.InitRange(aRefRange);
436 0 : formula::FormulaTokenRef xTok(new ScMatrixRangeToken(pMat, aRef));
437 0 : aCachedTokens.insert(CachedTokensType::value_type(p, xTok));
438 0 : aCode2.AddToken(*xTok);
439 : }
440 : else
441 : {
442 0 : ScMatrixToken aTok(pMat);
443 0 : aCode2.AddToken(aTok);
444 0 : }
445 : }
446 0 : break;
447 : default:
448 0 : aCode2.AddToken(*p);
449 : }
450 : }
451 :
452 0 : ScFormulaCell* pDest = rDoc.GetFormulaCell(aTmpPos);
453 0 : if (!pDest)
454 0 : return false;
455 :
456 0 : ScCompiler aComp(&rDoc, aTmpPos, aCode2);
457 0 : aComp.CompileTokenArray();
458 0 : ScInterpreter aInterpreter(pDest, &rDoc, aTmpPos, aCode2);
459 0 : aInterpreter.Interpret();
460 0 : aResults.push_back(aInterpreter.GetResultToken());
461 0 : } // for loop end (xGroup->mnLength)
462 :
463 0 : if (!aResults.empty())
464 0 : rDoc.SetFormulaResults(rTopPos, &aResults[0], aResults.size());
465 :
466 0 : return true;
467 : }
468 :
469 : #if USE_DUMMY_INTERPRETER
470 : class FormulaGroupInterpreterDummy : public FormulaGroupInterpreter
471 : {
472 : enum Mode {
473 : WRITE_OUTPUT = 0
474 : };
475 : Mode meMode;
476 : public:
477 : FormulaGroupInterpreterDummy()
478 : {
479 : const char *pValue = getenv("FORMULA_GROUP_DUMMY");
480 : meMode = static_cast<Mode>(OString(pValue, strlen(pValue)).toInt32());
481 : SAL_INFO("sc.formulagroup", "Using Dummy Formula Group interpreter mode " << (int)meMode);
482 : }
483 :
484 : virtual ScMatrixRef inverseMatrix(const ScMatrix& /*rMat*/)
485 : {
486 : return ScMatrixRef();
487 : }
488 :
489 : virtual bool interpret(ScDocument& rDoc, const ScAddress& rTopPos,
490 : const ScFormulaCellGroupRef& xGroup,
491 : ScTokenArray& rCode)
492 : {
493 : (void)rCode;
494 :
495 : // Write simple data back into the sheet
496 : if (meMode == WRITE_OUTPUT)
497 : {
498 : boost::scoped_array<double> pDoubles(new double[xGroup->mnLength]);
499 : for (sal_Int32 i = 0; i < xGroup->mnLength; i++)
500 : pDoubles[i] = 42.0 + i;
501 : rDoc.SetFormulaResults(rTopPos, pDoubles.get(), xGroup->mnLength);
502 : }
503 : return true;
504 : }
505 : };
506 :
507 : #endif
508 :
509 : FormulaGroupInterpreter *FormulaGroupInterpreter::msInstance = NULL;
510 :
511 : /// load and/or configure the correct formula group interpreter
512 0 : FormulaGroupInterpreter *FormulaGroupInterpreter::getStatic()
513 : {
514 : #if USE_DUMMY_INTERPRETER
515 : if (getenv("FORMULA_GROUP_DUMMY"))
516 : {
517 : delete msInstance;
518 : return msInstance = new sc::FormulaGroupInterpreterDummy();
519 : }
520 : #endif
521 :
522 0 : if ( !msInstance )
523 : {
524 0 : const ScCalcConfig& rConfig = ScInterpreter::GetGlobalConfig();
525 0 : if (rConfig.mbOpenCLEnabled)
526 0 : switchOpenCLDevice(rConfig.maOpenCLDevice, rConfig.mbOpenCLAutoSelect, false);
527 :
528 0 : if ( !msInstance ) // software fallback
529 : {
530 : SAL_INFO("sc.formulagroup", "Create S/W interpreter");
531 0 : msInstance = new sc::FormulaGroupInterpreterSoftware();
532 : }
533 : }
534 :
535 0 : return msInstance;
536 : }
537 :
538 0 : void FormulaGroupInterpreter::fillOpenCLInfo(std::vector<OpenCLPlatformInfo>& rPlatforms)
539 : {
540 : #if !HAVE_FEATURE_OPENCL
541 : (void) rPlatforms;
542 : #else
543 : const std::vector<sc::OpenCLPlatformInfo>& rPlatformsFromWrapper =
544 0 : sc::opencl::fillOpenCLInfo();
545 :
546 0 : rPlatforms.assign(rPlatformsFromWrapper.begin(), rPlatformsFromWrapper.end());
547 : #endif
548 0 : }
549 :
550 446 : bool FormulaGroupInterpreter::switchOpenCLDevice(const OUString& rDeviceId, bool bAutoSelect, bool bForceEvaluation)
551 : {
552 446 : bool bOpenCLEnabled = ScInterpreter::GetGlobalConfig().mbOpenCLEnabled;
553 446 : if (!bOpenCLEnabled || rDeviceId == OPENCL_SOFTWARE_DEVICE_CONFIG_NAME)
554 : {
555 0 : if(msInstance)
556 : {
557 : // if we already have a software interpreter don't delete it
558 0 : if(dynamic_cast<sc::FormulaGroupInterpreterSoftware*>(msInstance))
559 0 : return true;
560 :
561 0 : delete msInstance;
562 : }
563 :
564 0 : msInstance = new sc::FormulaGroupInterpreterSoftware();
565 0 : return true;
566 : }
567 : #if HAVE_FEATURE_OPENCL
568 446 : bool bSuccess = sc::opencl::switchOpenCLDevice(&rDeviceId, bAutoSelect, bForceEvaluation);
569 446 : if(!bSuccess)
570 446 : return false;
571 : #else
572 : (void) bAutoSelect;
573 : #endif
574 :
575 0 : delete msInstance;
576 0 : msInstance = NULL;
577 :
578 : #if HAVE_FEATURE_OPENCL
579 0 : if ( ScInterpreter::GetGlobalConfig().mbOpenCLEnabled )
580 : {
581 0 : msInstance = new sc::opencl::FormulaGroupInterpreterOpenCL();
582 0 : return msInstance != NULL;
583 : }
584 : #else
585 : (void) bForceEvaluation;
586 : #endif
587 0 : return false;
588 : }
589 :
590 0 : void FormulaGroupInterpreter::getOpenCLDeviceInfo(sal_Int32& rDeviceId, sal_Int32& rPlatformId)
591 : {
592 0 : rDeviceId = -1;
593 0 : rPlatformId = -1;
594 0 : bool bOpenCLEnabled = ScInterpreter::GetGlobalConfig().mbOpenCLEnabled;
595 0 : if(!bOpenCLEnabled)
596 0 : return;
597 :
598 : #if HAVE_FEATURE_OPENCL
599 :
600 0 : size_t aDeviceId = static_cast<size_t>(-1);
601 0 : size_t aPlatformId = static_cast<size_t>(-1);
602 :
603 0 : sc::opencl::getOpenCLDeviceInfo(aDeviceId, aPlatformId);
604 0 : rDeviceId = aDeviceId;
605 0 : rPlatformId = aPlatformId;
606 : #endif
607 : }
608 :
609 446 : void FormulaGroupInterpreter::enableOpenCL(bool bEnable)
610 : {
611 446 : ScCalcConfig aConfig = ScInterpreter::GetGlobalConfig();
612 446 : aConfig.mbOpenCLEnabled = bEnable;
613 446 : ScInterpreter::SetGlobalConfig(aConfig);
614 446 : }
615 :
616 228 : } // namespace sc
617 :
618 : /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|