source: libcfa/src/collections/string_res.cfa@ c298079

Last change on this file since c298079 was 0860d9c, checked in by Michael Brooks <mlbrooks@…>, 2 years ago

Fix read-to-variable-length-string cases when internal buffer fills.

Also fix read-to-cstring ability to give no-exception cases when an entire buffer fills.

The added test cases run, and fail, when run against prior libcfa.
Doing so illustrates a CFA-string-level bug.
Doing so illustrates a C-string-level changed semantics.

At the CFA-string level, the bug was, when reading strings of just the right length,
what should be two reads ("abc" then "def") gets mashed into one ("abcdef").
These cases are clearly bugs because a test program that just echoes chuncks of delimeted input would do so inaccurately.
They're just hard to drive because the relevant chunk lengths are implementation-dependent, and sometimes big.

At the C-string level, the semantic change concerns when to throw the cstring_length exception.
By this change, make the original semantics,
"An exception means the maximum number of characters was read," into
"An exception means that if the buffer were larger, then more characters would have been read."

The added test cases cover the respective stop conditions for manipulator state "%s", include, exclude, getline, and getline/delimiter.

  • Property mode set to 100644
File size: 42.0 KB
Line 
1//
2// Cforall Version 1.0.0 Copyright (C) 2016 University of Waterloo
3//
4// The contents of this file are covered under the licence agreement in the
5// file "LICENCE" distributed with Cforall.
6//
7// string_res -- variable-length, mutable run of text, with resource semantics
8//
9// Author : Michael L. Brooks
10// Created On : Fri Sep 03 11:00:00 2021
11// Last Modified By : Peter A. Buhr
12// Last Modified On : Mon Aug 14 18:06:01 2023
13// Update Count : 12
14//
15
16#include "string_res.hfa"
17#include "string_sharectx.hfa"
18#include "stdlib.hfa"
19#include <ctype.h>
20
21// Workaround for observed performance penalty from calling CFA's alloc.
22// Workaround is: EndVbyte = TEMP_ALLOC(char, CurrSize)
23// Should be: EndVbyte = alloc(CurrSize)
24#define TEMP_ALLOC(T, n) (( T* ) malloc( n * sizeof( T ) ))
25
26#include <assert.h>
27
28//######################### VbyteHeap "header" #########################
29
30#ifdef VbyteDebug
31HandleNode *HeaderPtr;
32#endif // VbyteDebug
33
34struct VbyteHeap {
35
36 int NoOfCompactions; // number of compactions of the byte area
37 int NoOfExtensions; // number of extensions in the size of the byte area
38 int NoOfReductions; // number of reductions in the size of the byte area
39
40 int InitSize; // initial number of bytes in the byte-string area
41 int CurrSize; // current number of bytes in the byte-string area
42 char *StartVbyte; // pointer to the `st byte of the start of the byte-string area
43 char *EndVbyte; // pointer to the next byte after the end of the currently used portion of byte-string area
44 void *ExtVbyte; // pointer to the next byte after the end of the byte-string area
45
46 HandleNode Header; // header node for handle list
47}; // VbyteHeap
48
49
50static void compaction( VbyteHeap & ); // compaction of the byte area
51static void garbage( VbyteHeap &, int ); // garbage collect the byte area
52static void extend( VbyteHeap &, int ); // extend the size of the byte area
53static void reduce( VbyteHeap &, int ); // reduce the size of the byte area
54
55static void ?{}( VbyteHeap &, size_t = 1000 );
56static void ^?{}( VbyteHeap & );
57
58static int ByteCmp( char *, int, int, char *, int, int ); // compare 2 blocks of bytes
59static char *VbyteAlloc( VbyteHeap &, int ); // allocate a block bytes in the heap
60static char *VbyteTryAdjustLast( VbyteHeap &, int );
61
62static void AddThisAfter( HandleNode &, HandleNode & );
63static void DeleteNode( HandleNode & );
64static void MoveThisAfter( HandleNode &, const HandleNode & ); // move current handle after parameter handle
65
66
67// Allocate the storage for the variable sized area and intialize the heap variables.
68
69static void ?{}( VbyteHeap & this, size_t Size ) with(this) {
70#ifdef VbyteDebug
71 serr | "enter:VbyteHeap::VbyteHeap, this:" | &this | " Size:" | Size;
72#endif // VbyteDebug
73 NoOfCompactions = NoOfExtensions = NoOfReductions = 0;
74 InitSize = CurrSize = Size;
75 StartVbyte = EndVbyte = TEMP_ALLOC(char, CurrSize);
76 ExtVbyte = (void *)( StartVbyte + CurrSize );
77 Header.flink = Header.blink = &Header;
78 Header.ulink = & this;
79#ifdef VbyteDebug
80 HeaderPtr = &Header;
81 serr | "exit:VbyteHeap::VbyteHeap, this:" | &this;
82#endif // VbyteDebug
83} // VbyteHeap
84
85
86// Release the dynamically allocated storage for the byte area.
87
88static void ^?{}( VbyteHeap & this ) with(this) {
89 free( StartVbyte );
90} // ~VbyteHeap
91
92
93//######################### HandleNode #########################
94
95
96// Create a handle node. The handle is not linked into the handle list. This is the responsibilitiy of the handle
97// creator.
98
99static void ?{}( HandleNode & this ) with(this) {
100#ifdef VbyteDebug
101 serr | "enter:HandleNode::HandleNode, this:" | &this;
102#endif // VbyteDebug
103 s = 0;
104 lnth = 0;
105#ifdef VbyteDebug
106 serr | "exit:HandleNode::HandleNode, this:" | &this;
107#endif // VbyteDebug
108} // HandleNode
109
110// Create a handle node. The handle is linked into the handle list at the end. This means that this handle will NOT be
111// in order by string address, but this is not a problem because a string with length zero does nothing during garbage
112// collection.
113
114static void ?{}( HandleNode & this, VbyteHeap & vh ) with(this) {
115#ifdef VbyteDebug
116 serr | "enter:HandleNode::HandleNode, this:" | &this;
117#endif // VbyteDebug
118 s = 0;
119 lnth = 0;
120 ulink = &vh;
121 AddThisAfter( this, *vh.Header.blink );
122#ifdef VbyteDebug
123 serr | "exit:HandleNode::HandleNode, this:" | &this;
124#endif // VbyteDebug
125} // HandleNode
126
127
128// Delete a node from the handle list by unchaining it from the list. If the handle node was allocated dynamically, it
129// is the responsibility of the creator to destroy it.
130
131static void ^?{}( HandleNode & this ) with(this) {
132#ifdef VbyteDebug
133 serr | "enter:HandleNode::~HandleNode, this:" | & this;
134 {
135 serr | nlOff;
136 serr | " lnth:" | lnth | " s:" | (void *)s | ",\"";
137 for ( i; lnth ) {
138 serr | s[i];
139 } // for
140 serr | "\" flink:" | flink | " blink:" | blink | nl;
141 serr | nlOn;
142 }
143#endif // VbyteDebug
144 DeleteNode( this );
145} // ~HandleNode
146
147
148//######################### String Sharing Context #########################
149
150static string_sharectx * ambient_string_sharectx; // fickle top of stack
151static string_sharectx default_string_sharectx = {NEW_SHARING}; // stable bottom of stack
152
153void ?{}( string_sharectx & this, StringSharectx_Mode mode ) with( this ) {
154 (older){ ambient_string_sharectx };
155 if ( mode == NEW_SHARING ) {
156 (activeHeap){ new( (size_t) 1000 ) };
157 } else {
158 verify( mode == NO_SHARING );
159 (activeHeap){ 0p };
160 }
161 ambient_string_sharectx = & this;
162}
163
164void ^?{}( string_sharectx & this ) with( this ) {
165 if ( activeHeap ) delete( activeHeap );
166
167 // unlink this from older-list starting from ambient_string_sharectx
168 // usually, this==ambient_string_sharectx and the loop runs zero times
169 string_sharectx *& c = ambient_string_sharectx;
170 while ( c != &this ) &c = &c->older; // find this
171 c = this.older; // unlink
172}
173
174//######################### String Resource #########################
175
176
177VbyteHeap * DEBUG_string_heap() {
178 assert( ambient_string_sharectx->activeHeap && "No sharing context is active" );
179 return ambient_string_sharectx->activeHeap;
180}
181
182size_t DEBUG_string_bytes_avail_until_gc( VbyteHeap * heap ) {
183 return ((char*)heap->ExtVbyte) - heap->EndVbyte;
184}
185
186size_t DEBUG_string_bytes_in_heap( VbyteHeap * heap ) {
187 return heap->CurrSize;
188}
189
190const char * DEBUG_string_heap_start( VbyteHeap * heap ) {
191 return heap->StartVbyte;
192}
193
194// Returns the size of the string in bytes
195size_t size(const string_res &s) with(s) {
196 return Handle.lnth;
197}
198
199// Output operator
200ofstream & ?|?(ofstream &out, const string_res &s) {
201 // CFA string is NOT null terminated, so print exactly lnth characters in a minimum width of 0.
202 out | wd( 0, s.Handle.lnth, s.Handle.s ) | nonl;
203 return out;
204}
205
206void ?|?(ofstream &out, const string_res &s) {
207 (ofstream &)(out | s); ends( out );
208}
209
210// Input operator
211ifstream & ?|?(ifstream &in, string_res &s) {
212
213 // Reading into a temp before assigning to s is near zero overhead in typical cases because of sharing.
214 // If s is a substring of something larger, simple assignment takes care of that case correctly.
215 // But directly reading a variable amount of text into the middle of a larger context is not practical.
216 string_res temp;
217
218 // Read in chunks. Often, one chunk is enough. Keep the string that accumulates chunks last in the heap,
219 // so available room is rest of heap. When a chunk fills the heap, force growth then take the next chunk.
220 for (bool cont = true; cont; ) {
221 cont = false;
222
223 // Append dummy content to temp, forcing expansion when applicable (occurs always on subsequent loops)
224 // length 2 ensures room for at least one real char, plus scanf/pipe-cstr's null terminator
225 temp += "--";
226 assert( temp.Handle.ulink->EndVbyte == temp.Handle.s + temp.Handle.lnth ); // last in heap
227
228 // reset, to overwrite the appended "--"
229 temp.Handle.lnth -= 2;
230 temp.Handle.ulink->EndVbyte -= 2;
231
232 // rest of heap is available to read into
233 int lenReadable = (char*)temp.Handle.ulink->ExtVbyte - temp.Handle.ulink->EndVbyte;
234 assert (lenReadable >= 2);
235
236 // get bytes
237 try {
238 in | wdi( lenReadable, temp.Handle.ulink->EndVbyte );
239 } catch (cstring_length*) {
240 cont = true;
241 }
242 int lenWasRead = strlen(temp.Handle.ulink->EndVbyte);
243
244 // update metadata
245 temp.Handle.lnth += lenWasRead;
246 temp.Handle.ulink->EndVbyte += lenWasRead;
247 }
248
249 s = temp;
250 return in;
251}
252
253void ?|?( ifstream & in, string_res & this ) {
254 (ifstream &)(in | this); ends( in );
255}
256
257ifstream & ?|?( ifstream & is, _Istream_Rstr f ) {
258 // .---------------,
259 // | | | | |...|0|0| null terminator and guard if missing
260 // `---------------'
261 enum { gwd = 128 + 1, wd = gwd - 1 }; // guard and unguard width
262 char cstr[gwd]; // read in chunks
263 bool cont = false;
264
265 _Istream_Cstr cf = { cstr, (_Istream_str_base)f };
266 if ( ! cf.flags.rwd ) cf.wd = wd;
267
268 cstr[wd] = '\0'; // guard null terminate string
269 try {
270 is | cf;
271 } catch( cstring_length * ) {
272 cont = true;
273 } finally {
274 if ( ! cf.flags.ignore ) *(f.s) = cstr; // ok to initialize string
275 } // try
276 for ( ; cont; ) { // overflow read ?
277 cont = false;
278 try {
279 is | cf;
280 } catch( cstring_length * ) {
281 cont = true; // continue not allowed
282 } finally {
283 if ( ! cf.flags.ignore ) *(f.s) += cstr; // build string chunk at a time
284 } // try
285 } // for
286 return is;
287} // ?|?
288
289void ?|?( ifstream & in, _Istream_Rstr f ) {
290 (ifstream &)(in | f); ends( in );
291}
292
293
294// Empty constructor
295void ?{}(string_res &s) with(s) {
296 if( ambient_string_sharectx->activeHeap ) {
297 (Handle){ * ambient_string_sharectx->activeHeap };
298 (shareEditSet_owns_ulink){ false };
299 verify( Handle.s == 0p && Handle.lnth == 0 );
300 } else {
301 (Handle){ * new( (size_t) 10 ) }; // TODO: can I lazily avoid allocating for empty string
302 (shareEditSet_owns_ulink){ true };
303 Handle.s = Handle.ulink->StartVbyte;
304 verify( Handle.lnth == 0 );
305 }
306 s.shareEditSet_prev = &s;
307 s.shareEditSet_next = &s;
308}
309
310static void eagerCopyCtorHelper(string_res &s, const char* rhs, size_t rhslnth) with(s) {
311 if( ambient_string_sharectx->activeHeap ) {
312 (Handle){ * ambient_string_sharectx->activeHeap };
313 (shareEditSet_owns_ulink){ false };
314 } else {
315 (Handle){ * new( rhslnth ) };
316 (shareEditSet_owns_ulink){ true };
317 }
318 Handle.s = VbyteAlloc(*Handle.ulink, rhslnth);
319 Handle.lnth = rhslnth;
320 memmove( Handle.s, rhs, rhslnth );
321 s.shareEditSet_prev = &s;
322 s.shareEditSet_next = &s;
323}
324
325// Constructor from a raw buffer and size
326void ?{}(string_res &s, const char* rhs, size_t rhslnth) with(s) {
327 eagerCopyCtorHelper(s, rhs, rhslnth);
328}
329
330// private ctor (not in header): use specified heap (ignore ambient) and copy chars in
331void ?{}( string_res &s, VbyteHeap & heap, const char* rhs, size_t rhslnth ) with(s) {
332 (Handle){ heap };
333 Handle.s = VbyteAlloc(*Handle.ulink, rhslnth);
334 Handle.lnth = rhslnth;
335 (s.shareEditSet_owns_ulink){ false };
336 memmove( Handle.s, rhs, rhslnth );
337 s.shareEditSet_prev = &s;
338 s.shareEditSet_next = &s;
339}
340
341// General copy constructor
342void ?{}(string_res &s, const string_res & s2, StrResInitMode mode, size_t start, size_t end ) {
343
344 verify( start <= end && end <= s2.Handle.lnth );
345
346 if (s2.Handle.ulink != ambient_string_sharectx->activeHeap && mode == COPY_VALUE) {
347 // crossing heaps (including private): copy eagerly
348 eagerCopyCtorHelper(s, s2.Handle.s + start, end - start);
349 verify(s.shareEditSet_prev == &s);
350 verify(s.shareEditSet_next == &s);
351 } else {
352 (s.Handle){};
353 s.Handle.s = s2.Handle.s + start;
354 s.Handle.lnth = end - start;
355 s.Handle.ulink = s2.Handle.ulink;
356
357 AddThisAfter(s.Handle, s2.Handle ); // insert this handle after rhs handle
358 // ^ bug? skip others at early point in string
359
360 if (mode == COPY_VALUE) {
361 verify(s2.Handle.ulink == ambient_string_sharectx->activeHeap);
362 // requested logical copy in same heap: defer copy until write
363
364 (s.shareEditSet_owns_ulink){ false };
365
366 // make s alone in its shareEditSet
367 s.shareEditSet_prev = &s;
368 s.shareEditSet_next = &s;
369 } else {
370 verify( mode == SHARE_EDITS );
371 // sharing edits with source forces same heap as source (ignore context)
372
373 (s.shareEditSet_owns_ulink){ s2.shareEditSet_owns_ulink };
374
375 // s2 is logically const but not implementation const
376 string_res & s2mod = (string_res &) s2;
377
378 // insert s after s2 on shareEditSet
379 s.shareEditSet_next = s2mod.shareEditSet_next;
380 s.shareEditSet_prev = &s2mod;
381 s.shareEditSet_next->shareEditSet_prev = &s;
382 s.shareEditSet_prev->shareEditSet_next = &s;
383 }
384 }
385}
386
387static void assignEditSet(string_res & this, string_res * shareEditSetStartPeer, string_res * shareEditSetEndPeer,
388 char * resultSesStart,
389 size_t resultSesLnth,
390 HandleNode * resultPadPosition, size_t bsize ) {
391
392 char * beforeBegin = shareEditSetStartPeer->Handle.s;
393 size_t beforeLen = this.Handle.s - beforeBegin;
394
395 char * afterBegin = this.Handle.s + this.Handle.lnth;
396 size_t afterLen = shareEditSetEndPeer->Handle.s + shareEditSetEndPeer->Handle.lnth - afterBegin;
397
398 size_t oldLnth = this.Handle.lnth;
399
400 this.Handle.s = resultSesStart + beforeLen;
401 this.Handle.lnth = bsize;
402 if (resultPadPosition)
403 MoveThisAfter( this.Handle, *resultPadPosition );
404
405 // adjust all substring string and handle locations, and check if any substring strings are outside the new base string
406 char *limit = resultSesStart + resultSesLnth;
407 for ( string_res * p = this.shareEditSet_next; p != &this; p = p->shareEditSet_next ) {
408 verify (p->Handle.s >= beforeBegin);
409 if ( p->Handle.s >= afterBegin ) {
410 verify ( p->Handle.s <= afterBegin + afterLen );
411 verify ( p->Handle.s + p->Handle.lnth <= afterBegin + afterLen );
412 // p starts after the edit
413 // take start and end as end-anchored
414 size_t startOffsetFromEnd = afterBegin + afterLen - p->Handle.s;
415 p->Handle.s = limit - startOffsetFromEnd;
416 // p->Handle.lnth unaffected
417 } else if ( p->Handle.s <= beforeBegin + beforeLen ) {
418 // p starts before, or at the start of, the edit
419 if ( p->Handle.s + p->Handle.lnth <= beforeBegin + beforeLen ) {
420 // p ends before the edit
421 // take end as start-anchored too
422 // p->Handle.lnth unaffected
423 } else if ( p->Handle.s + p->Handle.lnth < afterBegin ) {
424 // p ends during the edit; p does not include the last character replaced
425 // clip end of p to end at start of edit
426 p->Handle.lnth = beforeLen - ( p->Handle.s - beforeBegin );
427 } else {
428 // p ends after the edit
429 verify ( p->Handle.s + p->Handle.lnth <= afterBegin + afterLen );
430 // take end as end-anchored
431 // stretch-shrink p according to the edit
432 p->Handle.lnth += this.Handle.lnth;
433 p->Handle.lnth -= oldLnth;
434 }
435 // take start as start-anchored
436 size_t startOffsetFromStart = p->Handle.s - beforeBegin;
437 p->Handle.s = resultSesStart + startOffsetFromStart;
438 } else {
439 verify ( p->Handle.s < afterBegin );
440 // p starts during the edit
441 verify( p->Handle.s + p->Handle.lnth >= beforeBegin + beforeLen );
442 if ( p->Handle.s + p->Handle.lnth < afterBegin ) {
443 // p ends during the edit; p does not include the last character replaced
444 // set p to empty string at start of edit
445 p->Handle.s = this.Handle.s;
446 p->Handle.lnth = 0;
447 } else {
448 // p includes the end of the edit
449 // clip start of p to start at end of edit
450 int charsToClip = afterBegin - p->Handle.s;
451 p->Handle.s = this.Handle.s + this.Handle.lnth;
452 p->Handle.lnth -= charsToClip;
453 }
454 }
455 if (resultPadPosition)
456 MoveThisAfter( p->Handle, *resultPadPosition ); // move substring handle to maintain sorted order by string position
457 }
458}
459
460// traverse the share-edit set (SES) to recover the range of a base string to which `this` belongs
461static void locateInShareEditSet( string_res &this, string_res *&shareEditSetStartPeer, string_res *&shareEditSetEndPeer ) {
462 shareEditSetStartPeer = & this;
463 shareEditSetEndPeer = & this;
464 for (string_res * editPeer = this.shareEditSet_next; editPeer != &this; editPeer = editPeer->shareEditSet_next) {
465 if ( editPeer->Handle.s < shareEditSetStartPeer->Handle.s ) {
466 shareEditSetStartPeer = editPeer;
467 }
468 if ( shareEditSetEndPeer->Handle.s + shareEditSetEndPeer->Handle.lnth < editPeer->Handle.s + editPeer->Handle.lnth) {
469 shareEditSetEndPeer = editPeer;
470 }
471 }
472}
473
474static string_res & assign_(string_res &this, const char* buffer, size_t bsize, const string_res & valSrc) {
475
476 string_res * shareEditSetStartPeer;
477 string_res * shareEditSetEndPeer;
478 locateInShareEditSet( this, shareEditSetStartPeer, shareEditSetEndPeer );
479
480 verify( shareEditSetEndPeer->Handle.s >= shareEditSetStartPeer->Handle.s );
481 size_t origEditSetLength = shareEditSetEndPeer->Handle.s + shareEditSetEndPeer->Handle.lnth - shareEditSetStartPeer->Handle.s;
482 verify( origEditSetLength >= this.Handle.lnth );
483
484 if ( this.shareEditSet_owns_ulink ) { // assigning to private context
485 // ok to overwrite old value within LHS
486 char * prefixStartOrig = shareEditSetStartPeer->Handle.s;
487 int prefixLen = this.Handle.s - prefixStartOrig;
488 char * suffixStartOrig = this.Handle.s + this.Handle.lnth;
489 int suffixLen = shareEditSetEndPeer->Handle.s + shareEditSetEndPeer->Handle.lnth - suffixStartOrig;
490
491 int delta = bsize - this.Handle.lnth;
492 if ( char * oldBytes = VbyteTryAdjustLast( *this.Handle.ulink, delta ) ) {
493 // growing: copy from old to new
494 char * dest = VbyteAlloc( *this.Handle.ulink, origEditSetLength + delta );
495 char *destCursor = dest; memcpy(destCursor, prefixStartOrig, prefixLen);
496 destCursor += prefixLen; memcpy(destCursor, buffer , bsize );
497 destCursor += bsize; memcpy(destCursor, suffixStartOrig, suffixLen);
498 assignEditSet(this, shareEditSetStartPeer, shareEditSetEndPeer,
499 dest,
500 origEditSetLength + delta,
501 0p, bsize);
502 free( oldBytes );
503 } else {
504 // room is already allocated in-place: bubble suffix and overwite middle
505 memmove( suffixStartOrig + delta, suffixStartOrig, suffixLen );
506 memcpy( this.Handle.s, buffer, bsize );
507
508 assignEditSet(this, shareEditSetStartPeer, shareEditSetEndPeer,
509 shareEditSetStartPeer->Handle.s,
510 origEditSetLength + delta,
511 0p, bsize);
512 }
513
514 } else if ( // assigning to shared context
515 this.Handle.lnth == origEditSetLength && // overwriting entire run of SES
516 & valSrc && // sourcing from a managed string
517 valSrc.Handle.ulink == this.Handle.ulink ) { // sourcing from same heap
518
519 // SES's result will only use characters from the source string => reuse source
520 assignEditSet(this, shareEditSetStartPeer, shareEditSetEndPeer,
521 valSrc.Handle.s,
522 valSrc.Handle.lnth,
523 &((string_res&)valSrc).Handle, bsize);
524
525 } else {
526 // overwriting a proper substring of some string: mash characters from old and new together (copy on write)
527 // OR we are importing characters: need to copy eagerly (can't refer to source)
528
529 // full string is from start of shareEditSetStartPeer thru end of shareEditSetEndPeer
530 // `this` occurs in the middle of it, to be replaced
531 // build up the new text in `pasting`
532
533 string_res pasting = {
534 * this.Handle.ulink, // maintain same heap, regardless of context
535 shareEditSetStartPeer->Handle.s, // start of SES
536 this.Handle.s - shareEditSetStartPeer->Handle.s }; // length of SES, before this
537 append( pasting,
538 buffer, // start of replacement for this
539 bsize ); // length of replacement for this
540 append( pasting,
541 this.Handle.s + this.Handle.lnth, // start of SES after this
542 shareEditSetEndPeer->Handle.s + shareEditSetEndPeer->Handle.lnth -
543 (this.Handle.s + this.Handle.lnth) ); // length of SES, after this
544
545 // The above string building can trigger compaction.
546 // The reference points (that are arguments of the string building) may move during that building.
547 // From this point on, they are stable.
548
549 assignEditSet(this, shareEditSetStartPeer, shareEditSetEndPeer,
550 pasting.Handle.s,
551 pasting.Handle.lnth,
552 &pasting.Handle, bsize);
553 }
554
555 return this;
556}
557
558string_res & assign(string_res &this, const char* buffer, size_t bsize) {
559 return assign_(this, buffer, bsize, *0p);
560}
561
562string_res & ?=?(string_res &s, char other) {
563 return assign(s, &other, 1);
564}
565
566// Copy assignment operator
567string_res & ?=?(string_res & this, const string_res & rhs) with( this ) {
568 return assign_(this, rhs.Handle.s, rhs.Handle.lnth, rhs);
569}
570
571string_res & ?=?(string_res & this, string_res & rhs) with( this ) {
572 const string_res & rhs2 = rhs;
573 return this = rhs2;
574}
575
576
577// Destructor
578void ^?{}(string_res &s) with(s) {
579 // much delegated to implied ^VbyteSM
580
581 // sever s from its share-edit peers, if any (four no-ops when already solo)
582 s.shareEditSet_prev->shareEditSet_next = s.shareEditSet_next;
583 s.shareEditSet_next->shareEditSet_prev = s.shareEditSet_prev;
584 // s.shareEditSet_next = &s;
585 // s.shareEditSet_prev = &s;
586
587 if (shareEditSet_owns_ulink && s.shareEditSet_next == &s) { // last one out
588 delete( s.Handle.ulink );
589 }
590}
591
592
593// Returns the character at the given index
594// With unicode support, this may be different from just the byte at the given
595// offset from the start of the string.
596char ?[?](const string_res &s, size_t index) with(s) {
597 //TODO: Check if index is valid (no exceptions yet)
598 return Handle.s[index];
599}
600
601void assignAt(const string_res &s, size_t index, char val) {
602 string_res editZone = { s, SHARE_EDITS, index, index+1 };
603 assign(editZone, &val, 1);
604}
605
606
607///////////////////////////////////////////////////////////////////
608// Concatenation
609
610void append(string_res &str1, const char * buffer, size_t bsize) {
611 size_t clnth = str1.Handle.lnth + bsize;
612 if ( str1.Handle.s + str1.Handle.lnth == buffer ) { // already juxtapose ?
613 // no-op
614 } else { // must copy some text
615 if ( str1.Handle.s + str1.Handle.lnth == VbyteAlloc(*str1.Handle.ulink, 0) ) { // str1 at end of string area ?
616 VbyteAlloc( *str1.Handle.ulink, bsize ); // create room for 2nd part at the end of string area
617 } else { // copy the two parts
618 char * str1newBuf = VbyteAlloc( *str1.Handle.ulink, clnth );
619 char * str1oldBuf = str1.Handle.s; // must read after VbyteAlloc call in case it gs's
620 str1.Handle.s = str1newBuf;
621 memcpy( str1.Handle.s, str1oldBuf, str1.Handle.lnth );
622 } // if
623 memcpy( str1.Handle.s + str1.Handle.lnth, buffer, bsize );
624 } // if
625 str1.Handle.lnth = clnth;
626}
627
628void ?+=?(string_res &str1, const string_res &str2) {
629 append( str1, str2.Handle.s, str2.Handle.lnth );
630}
631
632void ?+=?(string_res &s, char other) {
633 append( s, &other, 1 );
634}
635
636
637
638
639
640//////////////////////////////////////////////////////////
641// Comparisons
642
643int cmp(const string_res &s1, const string_res &s2) {
644 // return 0;
645 int ans1 = memcmp(s1.Handle.s, s2.Handle.s, min(s1.Handle.lnth, s2.Handle.lnth));
646 if (ans1 != 0) return ans1;
647 return s1.Handle.lnth - s2.Handle.lnth;
648}
649
650bool ?==?(const string_res &s1, const string_res &s2) { return cmp(s1, s2) == 0; }
651bool ?!=?(const string_res &s1, const string_res &s2) { return cmp(s1, s2) != 0; }
652bool ?>? (const string_res &s1, const string_res &s2) { return cmp(s1, s2) > 0; }
653bool ?>=?(const string_res &s1, const string_res &s2) { return cmp(s1, s2) >= 0; }
654bool ?<=?(const string_res &s1, const string_res &s2) { return cmp(s1, s2) <= 0; }
655bool ?<? (const string_res &s1, const string_res &s2) { return cmp(s1, s2) < 0; }
656
657int cmp (const string_res &s1, const char* s2) {
658 string_res s2x = s2;
659 return cmp(s1, s2x);
660}
661
662bool ?==?(const string_res &s1, const char* s2) { return cmp(s1, s2) == 0; }
663bool ?!=?(const string_res &s1, const char* s2) { return cmp(s1, s2) != 0; }
664bool ?>? (const string_res &s1, const char* s2) { return cmp(s1, s2) > 0; }
665bool ?>=?(const string_res &s1, const char* s2) { return cmp(s1, s2) >= 0; }
666bool ?<=?(const string_res &s1, const char* s2) { return cmp(s1, s2) <= 0; }
667bool ?<? (const string_res &s1, const char* s2) { return cmp(s1, s2) < 0; }
668
669int cmp (const char* s1, const string_res & s2) {
670 string_res s1x = s1;
671 return cmp(s1x, s2);
672}
673
674bool ?==?(const char* s1, const string_res &s2) { return cmp(s1, s2) == 0; }
675bool ?!=?(const char* s1, const string_res &s2) { return cmp(s1, s2) != 0; }
676bool ?>? (const char* s1, const string_res &s2) { return cmp(s1, s2) > 0; }
677bool ?>=?(const char* s1, const string_res &s2) { return cmp(s1, s2) >= 0; }
678bool ?<=?(const char* s1, const string_res &s2) { return cmp(s1, s2) <= 0; }
679bool ?<? (const char* s1, const string_res &s2) { return cmp(s1, s2) < 0; }
680
681
682
683//////////////////////////////////////////////////////////
684// Search
685
686bool contains(const string_res &s, char ch) {
687 for ( i; size(s) ) {
688 if (s[i] == ch) return true;
689 }
690 return false;
691}
692
693int find(const string_res &s, char search) {
694 return findFrom(s, 0, search);
695}
696
697int findFrom(const string_res &s, size_t fromPos, char search) {
698 // FIXME: This paricular overload (find of single char) is optimized to use memchr.
699 // The general overload (find of string, memchr applying to its first character) and `contains` should be adjusted to match.
700 char * searchFrom = s.Handle.s + fromPos;
701 size_t searchLnth = s.Handle.lnth - fromPos;
702 int searchVal = search;
703 char * foundAt = (char *) memchr(searchFrom, searchVal, searchLnth);
704 if (foundAt == 0p) return s.Handle.lnth;
705 else return foundAt - s.Handle.s;
706}
707
708int find(const string_res &s, const string_res &search) {
709 return findFrom(s, 0, search);
710}
711
712int findFrom(const string_res &s, size_t fromPos, const string_res &search) {
713 return findFrom(s, fromPos, search.Handle.s, search.Handle.lnth);
714}
715
716int find(const string_res &s, const char* search) {
717 return findFrom(s, 0, search);
718}
719int findFrom(const string_res &s, size_t fromPos, const char* search) {
720 return findFrom(s, fromPos, search, strlen(search));
721}
722
723int find(const string_res &s, const char* search, size_t searchsize) {
724 return findFrom(s, 0, search, searchsize);
725}
726
727int findFrom(const string_res &s, size_t fromPos, const char* search, size_t searchsize) {
728
729 /* Remaining implementations essentially ported from Sunjay's work */
730
731
732 // FIXME: This is a naive algorithm. We probably want to switch to someting
733 // like Boyer-Moore in the future.
734 // https://en.wikipedia.org/wiki/String_searching_algorithm
735
736 // Always find the empty string
737 if (searchsize == 0) {
738 return 0;
739 }
740
741 for ( i; fromPos ~ s.Handle.lnth ) {
742 size_t remaining = s.Handle.lnth - i;
743 // Never going to find the search string if the remaining string is
744 // smaller than search
745 if (remaining < searchsize) {
746 break;
747 }
748
749 bool matched = true;
750 for ( j; searchsize ) {
751 if (search[j] != s.Handle.s[i + j]) {
752 matched = false;
753 break;
754 }
755 }
756 if (matched) {
757 return i;
758 }
759 }
760
761 return s.Handle.lnth;
762}
763
764bool includes(const string_res &s, const string_res &search) {
765 return includes(s, search.Handle.s, search.Handle.lnth);
766}
767
768bool includes(const string_res &s, const char* search) {
769 return includes(s, search, strlen(search));
770}
771
772bool includes(const string_res &s, const char* search, size_t searchsize) {
773 return find(s, search, searchsize) < s.Handle.lnth;
774}
775
776bool startsWith(const string_res &s, const string_res &prefix) {
777 return startsWith(s, prefix.Handle.s, prefix.Handle.lnth);
778}
779
780bool startsWith(const string_res &s, const char* prefix) {
781 return startsWith(s, prefix, strlen(prefix));
782}
783
784bool startsWith(const string_res &s, const char* prefix, size_t prefixsize) {
785 if (s.Handle.lnth < prefixsize) {
786 return false;
787 }
788 return memcmp(s.Handle.s, prefix, prefixsize) == 0;
789}
790
791bool endsWith(const string_res &s, const string_res &suffix) {
792 return endsWith(s, suffix.Handle.s, suffix.Handle.lnth);
793}
794
795bool endsWith(const string_res &s, const char* suffix) {
796 return endsWith(s, suffix, strlen(suffix));
797}
798
799bool endsWith(const string_res &s, const char* suffix, size_t suffixsize) {
800 if (s.Handle.lnth < suffixsize) {
801 return false;
802 }
803 // Amount to offset the bytes pointer so that we are comparing the end of s
804 // to suffix. s.bytes + offset should be the first byte to compare against suffix
805 size_t offset = s.Handle.lnth - suffixsize;
806 return memcmp(s.Handle.s + offset, suffix, suffixsize) == 0;
807}
808
809 /* Back to Mike's work */
810
811
812///////////////////////////////////////////////////////////////////////////
813// charclass, include, exclude
814
815void ?{}( charclass_res & this, const string_res & chars) {
816 (this){ chars.Handle.s, chars.Handle.lnth };
817}
818
819void ?{}( charclass_res & this, const char * chars ) {
820 (this){ chars, strlen(chars) };
821}
822
823void ?{}( charclass_res & this, const char * chars, size_t charssize ) {
824 (this.chars){ chars, charssize };
825 // now sort it ?
826}
827
828void ^?{}( charclass_res & this ) {
829 ^(this.chars){};
830}
831
832static bool test( const charclass_res & mask, char c ) {
833 // instead, use sorted char list?
834 return contains( mask.chars, c );
835}
836
837int exclude(const string_res &s, const charclass_res &mask) {
838 for ( i; size(s) ) {
839 if ( test(mask, s[i]) ) return i;
840 }
841 return size(s);
842}
843
844int include(const string_res &s, const charclass_res &mask) {
845 for ( i; size(s) ) {
846 if ( ! test(mask, s[i]) ) return i;
847 }
848 return size(s);
849}
850
851//######################### VbyteHeap "implementation" #########################
852
853
854// Add a new HandleNode node n after the current HandleNode node.
855
856static void AddThisAfter( HandleNode & this, HandleNode & n ) with(this) {
857#ifdef VbyteDebug
858 serr | "enter:AddThisAfter, this:" | &this | " n:" | &n;
859#endif // VbyteDebug
860 // Performance note: we are on the critical path here. MB has ensured that the verifies don't contribute to runtime (are compiled away, like they're supposed to be).
861 verify( n.ulink != 0p );
862 verify( this.ulink == n.ulink );
863 flink = n.flink;
864 blink = &n;
865 n.flink->blink = &this;
866 n.flink = &this;
867#ifdef VbyteDebug
868 {
869 serr | "HandleList:";
870 serr | nlOff;
871 for ( HandleNode *ni = HeaderPtr->flink; ni != HeaderPtr; ni = ni->flink ) {
872 serr | "\tnode:" | ni | " lnth:" | ni->lnth | " s:" | (void *)ni->s | ",\"";
873 for ( i; ni->lnth ) {
874 serr | ni->s[i];
875 } // for
876 serr | "\" flink:" | ni->flink | " blink:" | ni->blink | nl;
877 } // for
878 serr | nlOn;
879 }
880 serr | "exit:AddThisAfter";
881#endif // VbyteDebug
882} // AddThisAfter
883
884
885// Delete the current HandleNode node.
886
887static void DeleteNode( HandleNode & this ) with(this) {
888#ifdef VbyteDebug
889 serr | "enter:DeleteNode, this:" | &this;
890#endif // VbyteDebug
891 flink->blink = blink;
892 blink->flink = flink;
893#ifdef VbyteDebug
894 serr | "exit:DeleteNode";
895#endif // VbyteDebug
896} // DeleteNode
897
898
899
900// Allocates specified storage for a string from byte-string area. If not enough space remains to perform the
901// allocation, the garbage collection routine is called.
902
903static char * VbyteAlloc( VbyteHeap & this, int size ) with(this) {
904#ifdef VbyteDebug
905 serr | "enter:VbyteAlloc, size:" | size;
906#endif // VbyteDebug
907 uintptr_t NoBytes;
908 char *r;
909
910 NoBytes = ( uintptr_t )EndVbyte + size;
911 if ( NoBytes > ( uintptr_t )ExtVbyte ) { // enough room for new byte-string ?
912 garbage( this, size ); // firer up the garbage collector
913 verify( (( uintptr_t )EndVbyte + size) <= ( uintptr_t )ExtVbyte && "garbage run did not free up required space" );
914 } // if
915 r = EndVbyte;
916 EndVbyte += size;
917#ifdef VbyteDebug
918 serr | "exit:VbyteAlloc, r:" | (void *)r | " EndVbyte:" | (void *)EndVbyte | " ExtVbyte:" | ExtVbyte;
919#endif // VbyteDebug
920 return r;
921} // VbyteAlloc
922
923
924// Adjusts the last allocation in this heap by delta bytes, or resets this heap to be able to offer
925// new allocations of its original size + delta bytes. Positive delta means bigger;
926// negative means smaller. A null return indicates that the original heap location has room for
927// the requested growth. A non-null return indicates that copying to a new location is required
928// but has not been done; the returned value is the old heap storage location; `this` heap is
929// modified to reference the new location. In the copy-requred case, the caller should use
930// VbyteAlloc to claim the new space, while doing optimal copying from old to new, then free old.
931
932static char * VbyteTryAdjustLast( VbyteHeap & this, int delta ) with(this) {
933
934 if ( ( uintptr_t )EndVbyte + delta <= ( uintptr_t )ExtVbyte ) {
935 // room available
936 EndVbyte += delta;
937 return 0p;
938 }
939
940 char *oldBytes = StartVbyte;
941
942 NoOfExtensions += 1;
943 CurrSize *= 2;
944 StartVbyte = EndVbyte = TEMP_ALLOC(char, CurrSize);
945 ExtVbyte = StartVbyte + CurrSize;
946
947 return oldBytes;
948}
949
950
951// Move an existing HandleNode node h somewhere after the current HandleNode node so that it is in ascending order by
952// the address in the byte string area.
953
954static void MoveThisAfter( HandleNode & this, const HandleNode & h ) with(this) {
955#ifdef VbyteDebug
956 serr | "enter:MoveThisAfter, this:" | & this | " h:" | & h;
957#endif // VbyteDebug
958 verify( h.ulink != 0p );
959 verify( this.ulink == h.ulink );
960 if ( s < h.s ) { // check argument values
961 // serr | "VbyteSM: Error - Cannot move byte string starting at:" | s | " after byte string starting at:"
962 // | ( h->s ) | " and keep handles in ascending order";
963 // exit(-1 );
964 verify( 0 && "VbyteSM: Error - Cannot move byte strings as requested and keep handles in ascending order");
965 } // if
966
967 HandleNode *i;
968 for ( i = h.flink; i->s != 0 && s > ( i->s ); i = i->flink ); // find the position for this node after h
969 if ( & this != i->blink ) {
970 DeleteNode( this );
971 AddThisAfter( this, *i->blink );
972 } // if
973#ifdef VbyteDebug
974 {
975 serr | "HandleList:";
976 serr | nlOff;
977 for ( HandleNode *n = HeaderPtr->flink; n != HeaderPtr; n = n->flink ) {
978 serr | "\tnode:" | n | " lnth:" | n->lnth | " s:" | (void *)n->s | ",\"";
979 for ( i; n->lnth ) {
980 serr | n->s[i];
981 } // for
982 serr | "\" flink:" | n->flink | " blink:" | n->blink | nl;
983 } // for
984 serr | nlOn;
985 }
986 serr | "exit:MoveThisAfter";
987#endif // VbyteDebug
988} // MoveThisAfter
989
990
991
992
993
994//######################### VbyteHeap #########################
995
996// Compare two byte strings in the byte-string area. The routine returns the following values:
997//
998// 1 => Src1-byte-string > Src2-byte-string
999// 0 => Src1-byte-string = Src2-byte-string
1000// -1 => Src1-byte-string < Src2-byte-string
1001
1002int ByteCmp( char *Src1, int Src1Start, int Src1Lnth, char *Src2, int Src2Start, int Src2Lnth ) {
1003#ifdef VbyteDebug
1004 serr | "enter:ByteCmp, Src1Start:" | Src1Start | " Src1Lnth:" | Src1Lnth | " Src2Start:" | Src2Start | " Src2Lnth:" | Src2Lnth;
1005#endif // VbyteDebug
1006 int cmp;
1007
1008 CharZip: for ( int i = 0; ; i += 1 ) {
1009 if ( i == Src2Lnth - 1 ) {
1010 for ( ; ; i += 1 ) {
1011 if ( i == Src1Lnth - 1 ) {
1012 cmp = 0;
1013 break CharZip;
1014 } // exit
1015 if ( Src1[Src1Start + i] != ' ') {
1016 // SUSPECTED BUG: this could be be why Peter got the bug report about == " " (why is this case here at all?)
1017 cmp = 1;
1018 break CharZip;
1019 } // exit
1020 } // for
1021 } // exit
1022 if ( i == Src1Lnth - 1 ) {
1023 for ( ; ; i += 1 ) {
1024 if ( i == Src2Lnth - 1 ) {
1025 cmp = 0;
1026 break CharZip;
1027 } // exit
1028 if ( Src2[Src2Start + i] != ' ') {
1029 cmp = -1;
1030 break CharZip;
1031 } // exit
1032 } // for
1033 } // exit
1034 if ( Src2[Src2Start + i] != Src1[Src1Start+ i]) {
1035 cmp = Src1[Src1Start + i] > Src2[Src2Start + i] ? 1 : -1;
1036 break CharZip;
1037 } // exit
1038 } // for
1039#ifdef VbyteDebug
1040 serr | "exit:ByteCmp, cmp:" | cmp;
1041#endif // VbyteDebug
1042 return cmp;
1043} // ByteCmp
1044
1045
1046// The compaction moves all of the byte strings currently in use to the beginning of the byte-string area and modifies
1047// the handles to reflect the new positions of the byte strings. Compaction assumes that the handle list is in ascending
1048// order by pointers into the byte-string area. The strings associated with substrings do not have to be moved because
1049// the containing string has been moved. Hence, they only require that their string pointers be adjusted.
1050
1051void compaction(VbyteHeap & this) with(this) {
1052 HandleNode *h;
1053 char *obase, *nbase, *limit;
1054
1055 NoOfCompactions += 1;
1056 EndVbyte = StartVbyte;
1057 h = Header.flink; // ignore header node
1058 for () {
1059 memmove( EndVbyte, h->s, h->lnth );
1060 obase = h->s;
1061 h->s = EndVbyte;
1062 nbase = h->s;
1063 EndVbyte += h->lnth;
1064 limit = obase + h->lnth;
1065 h = h->flink;
1066
1067 // check if any substrings are allocated within a string
1068
1069 for () {
1070 if ( h == &Header ) break; // end of header list ?
1071 if ( h->s >= limit ) break; // outside of current string ?
1072 h->s = nbase + (( uintptr_t )h->s - ( uintptr_t )obase );
1073 h = h->flink;
1074 } // for
1075 if ( h == &Header ) break; // end of header list ?
1076 } // for
1077} // compaction
1078
1079
1080static double heap_expansion_freespace_threshold = 0.1; // default inherited from prior work: expand heap when less than 10% "free" (i.e. garbage)
1081 // probably an unreasonable default, but need to assess early-round tests on changing it
1082
1083void TUNING_set_string_heap_liveness_threshold( double val ) {
1084 heap_expansion_freespace_threshold = 1.0 - val;
1085}
1086
1087
1088// Garbage determines the amount of free space left in the heap and then reduces, leave the same, or extends the size of
1089// the heap. The heap is then compacted in the existing heap or into the newly allocated heap.
1090
1091void garbage(VbyteHeap & this, int minreq ) with(this) {
1092#ifdef VbyteDebug
1093 serr | "enter:garbage";
1094 {
1095 serr | "HandleList:";
1096 for ( HandleNode *n = Header.flink; n != &Header; n = n->flink ) {
1097 serr | nlOff;
1098 serr | "\tnode:" | n | " lnth:" | n->lnth | " s:" | (void *)n->s | ",\"";
1099 for ( i; n->lnth ) {
1100 serr | n->s[i];
1101 } // for
1102 serr | nlOn;
1103 serr | "\" flink:" | n->flink | " blink:" | n->blink;
1104 } // for
1105 }
1106#endif // VbyteDebug
1107 int AmountUsed, AmountFree;
1108
1109 AmountUsed = 0;
1110 for ( HandleNode *i = Header.flink; i != &Header; i = i->flink ) { // calculate amount of byte area used
1111 AmountUsed += i->lnth;
1112 } // for
1113 AmountFree = ( uintptr_t )ExtVbyte - ( uintptr_t )StartVbyte - AmountUsed;
1114
1115 if ( ( double ) AmountFree < ( CurrSize * heap_expansion_freespace_threshold ) || AmountFree < minreq ) { // free space less than threshold or not enough to serve cur request
1116
1117 extend( this, max( CurrSize, minreq ) ); // extend the heap
1118
1119 // Peter says, "This needs work before it should be used."
1120 // } else if ( AmountFree > CurrSize / 2 ) { // free space greater than 3 times the initial allocation ?
1121 // reduce(( AmountFree / CurrSize - 3 ) * CurrSize ); // reduce the memory
1122
1123 // `extend` implies a `compaction` during the copy
1124
1125 } else {
1126 compaction(this); // in-place
1127 }// if
1128#ifdef VbyteDebug
1129 {
1130 serr | "HandleList:";
1131 for ( HandleNode *n = Header.flink; n != &Header; n = n->flink ) {
1132 serr | nlOff;
1133 serr | "\tnode:" | n | " lnth:" | n->lnth | " s:" | (void *)n->s | ",\"";
1134 for ( i; n->lnth ) {
1135 serr | n->s[i];
1136 } // for
1137 serr | nlOn;
1138 serr | "\" flink:" | n->flink | " blink:" | n->blink;
1139 } // for
1140 }
1141 serr | "exit:garbage";
1142#endif // VbyteDebug
1143} // garbage
1144
1145#undef VbyteDebug
1146
1147
1148
1149// Extend the size of the byte-string area by creating a new area and copying the old area into it. The old byte-string
1150// area is deleted.
1151
1152void extend( VbyteHeap & this, int size ) with (this) {
1153#ifdef VbyteDebug
1154 serr | "enter:extend, size:" | size;
1155#endif // VbyteDebug
1156 char *OldStartVbyte;
1157
1158 NoOfExtensions += 1;
1159 OldStartVbyte = StartVbyte; // save previous byte area
1160
1161 CurrSize += size > InitSize ? size : InitSize; // minimum extension, initial size
1162 StartVbyte = EndVbyte = TEMP_ALLOC(char, CurrSize);
1163 ExtVbyte = (void *)( StartVbyte + CurrSize );
1164 compaction(this); // copy from old heap to new & adjust pointers to new heap
1165 free( OldStartVbyte ); // release old heap
1166#ifdef VbyteDebug
1167 serr | "exit:extend, CurrSize:" | CurrSize;
1168#endif // VbyteDebug
1169} // extend
1170
1171//WIP
1172#if 0
1173
1174// Extend the size of the byte-string area by creating a new area and copying the old area into it. The old byte-string
1175// area is deleted.
1176
1177void VbyteHeap::reduce( int size ) {
1178#ifdef VbyteDebug
1179 serr | "enter:reduce, size:" | size;
1180#endif // VbyteDebug
1181 char *OldStartVbyte;
1182
1183 NoOfReductions += 1;
1184 OldStartVbyte = StartVbyte; // save previous byte area
1185
1186 CurrSize -= size;
1187 StartVbyte = EndVbyte = new char[CurrSize];
1188 ExtVbyte = (void *)( StartVbyte + CurrSize );
1189 compaction(); // copy from old heap to new & adjust pointers to new heap
1190 delete OldStartVbyte; // release old heap
1191#ifdef VbyteDebug
1192 serr | "exit:reduce, CurrSize:" | CurrSize;
1193#endif // VbyteDebug
1194} // reduce
1195
1196
1197#endif
Note: See TracBrowser for help on using the repository browser.