Simple Tokenizer & Segfault tutorial

Here I discuss the process of implementing and debugging a tokenizer based
on the SML (Standard ML) parser tutorial from Scott Smith
(http://www.cs.jhu.edu/~scott/pl/lectures/parsing.html) in ATS, without
using linear types. This was done in a bit of a hurry, so certainly there
may be some bugs due to little testing. Once complete, I will include a
more polished tutorial in its entirety. Comments on style are also welcome.

The fact that I got a segmentation fault when writing simple code probably
indicates 1) I need practice… and 2) that dealing with strings is
probably a good case for using linear types.

First, we use Hongwei Xi’s implementation of the string stream mentioned in
the tutorial
(https://groups.google.com/forum/?fromgroups=#!topic/ats-lang-users/oFuXRr4K8ts).

After our first round of implementation, and implementing (potentially
redundant) functions like toCaps, etc, we arrive at the following code.
However, I want to stress that the implementation of string_caps is not
good because it creates references that need to be cleared by the GC very
frequently (but it works), as noted in the ATS Book section on references.
For my original try which I did late at night (possibly only part of the
problem), see here: http://pastebin.com/M0ckExtX

(* ********************************* Begin CODE
******************************** *)

staload "prelude/SATS/string.sats"
staload "prelude/SATS/printf.sats"
staload “libc/SATS/stdio.sats”

#define NUL '\000’
exception UnforseenLexeme of ()

typedef strstream = '{
peek = () - char,
next = () - void,
prev = () - void
//cose() - if using ptrs
}

datatype GRTOK =
| TKgene of string
| TKand of ()
| TKor of ()
| TKlpar of ()
| TKrpar of ()
| TKEND of ()

extern
fun toCaps(c:char): char

extern
fun strstream_make (str: string): strstream

extern
fun getToken (ss: strstream): GRTOK

fn string_of_string1 {n:nat} (str : string n): string = str

//Get the minimum of two sizes, assert the minimum via type
fun {m,n:nat} minim (x:size_t m, y:size_t n): size_t(min(m,n)) = if x < y
then x else y

(*Hongwei’s implementation of a string stream: *)
implement
strstream_make (str:string) =
let
val [n:int] str = string1_of_string (str)
val n = string_length (str)
val pos = ref<sizeLte(n)> (size1_of_int1(0))
val fpeek = lam () : char =
let
val i = !pos
in if i < n then str[i] else NUL
end // end of [lam]
val fnext = lam () : void =
let
val i = !pos
in if i < n then !pos := i + 1
end // end of [lam]
val fprev = lam () : void =
let
val i = !pos
in if i > 0 then !pos := i - 1
end // end of [lam]
in '{ peek= fpeek, next= fnext, prev= fprev}
end // end of [strstream_make]

//Need an alternative as this requires a lot of GC:
fun string_caps(str: string): string =
let
val strstr = strstream_make(str)
fun loop(): string =
let
val x = strstr.peek()
val x = toCaps(x)
in
if x <> NUL then
let
val _ = strstr.next()
val xs = loop()
val x = string_of_string1(tostring(x))
in
x + xs
end
else string1_of_string("")
end
in
loop()
end

(* Assumes ASCII *)

fun print_tks(ss: strstream): void =
let
fun loop(): void =
let
val tk = getToken(ss)
in case+ tk of
| TKEND() => print(“END\n”)
| TKand() => (print ("AND "); loop())
| TKor() => (print("OR “); loop())
| TKlpar() => (print(”( “); loop())
| TKrpar() => (print(”) “); loop())
| TKgene(g) => (print(g+” "); loop())
end
in
loop()
end

implement
toCaps©: char = if (c >= ‘a’ && c <= ‘z’) then
char_of_int(int_of_char©-32) else c

fn isAnd(str: string): bool = if (str = “AND”) || (str = “&”)|| str = "&&"
then true else false

fn isOr(str: string): bool = if (str = “OR”) || str = “|” || str = "||"
then true else false

fn isGeneCh(c:char): bool = (c >= ‘0’ && c <= ‘9’) || (c >= ‘A’ && c <= ‘Z’)
|| (c >= ‘a’ && c <= ‘z’) || c = ‘_’ || c = ‘.’ || c = ‘-’

//AND, OR, and, or, &, |
fn isBoolCh(c:char): bool = c = ‘a’ || c = ‘A’ || c = ‘n’ || c = ‘N’ || c =
‘d’ || c = ‘D’
|| c = ‘o’ || c = ‘O’ || c = ‘r’ || c = ‘R’ || c = ‘|’ || c = ‘&’

fn isBool1stCh(c:char): bool = c = ‘a’ || c = ‘A’|| c = ‘o’ || c = ‘O’ || c
= ‘|’ || c = ‘&’

fn stopChar(c:char): bool = c = ‘\000’

//This should probably be changed, but we assume these are the above are
the only
//language letters.
fn isWhiteSpace(c:char): bool = not (stopChar© orelse isGeneCh© orelse
isBoolCh© orelse (c = ‘(’) orelse (c = ‘)’) )

implement
getToken (ss: strstream): GRTOK =
let
val c = ss.peek()
val _ = ss.next(); //need to be fun1?
fun whileCharTst(chtst: char - bool): string =
if chtst© then (ss.next(); tostring© + whileCharTst(chtst)) else
""
in
if c = ‘\000’ then TKEND()
else if c = ‘(’ then TKlpar()
else if c = ‘)’ then TKrpar()
//genes and booleans could share letters … need to fix
else if isBool1stCh© then
let
val logword = whileCharTst(isBoolCh)
in //BEWARE of potential problems of function evaluation in situ…
if isAnd(string_caps(logword)) then TKand ()
else if isOr(string_caps(logword)) then TKor ()
else TKgene(logword) //Assume gene for now, e.g. could be "AND&|OR"
end
else if isGeneCh© then TKgene(whileCharTst(isGeneCh)) //Build a
string
else if isWhiteSpace© then
let
val _ = whileCharTst(isWhiteSpace)
in
getToken(ss)
end
else $raise UnforseenLexeme ()
end

(* ********************************* End CODE
******************************** *)

To test, do the following:
val teststr = "Gene1 AND (Gene2) or Gene3 or Gene4 and Gene5"
val teststrstr = strstream_make(teststr)
val _ = print_tks(teststrstr)
//This will result in a segmentation fault.

Now, looking at gdb (debugger) output we see the following:

Starting program: /media/RAID5/share/ATS_learning/toCNF asdfs
Program received signal SIGSEGV, Segmentation fault.
_int_malloc (av=0x7ffff7dd3720, bytes=2) at malloc.c:3883
3883 malloc.c: No such file or directory.

This is not terribly instructive to me at least, but it seems to indicate
there is some problem with memory allocationdue to the seg fault occurring
in a call to malloc.

After appropriately placing several println! statements, we may begin to
notice that, among other things, the value ‘c’ in getToken() is not being
updated by calls to whileCharTst(). This is because c is being redefined in
a “let clause” which has local scope; there may be other scope-related
issues to the original formulation. This is the key problem, and once we do
this and a few other fixes, we arrive at the following function for
getToken():

(*
)
(
getToken() version 2
*)
implement
getToken (ss: strstream): GRTOK =
let
val c = ss.peek()
val _ = ss.next(); //need to be fun1?
fun whileCharTst(chtst: char - bool): string =
let
val cw = ss.peek()
in
if chtst(cw) then
let
val _ = ss.next()
//val _ = println!(cw)
in
tostring(cw) + whileCharTst(chtst)
end
else (ss.next(); “”) //Need to theck next() stop condition
end
in
if c = ‘\000’ then TKEND()
else if c = ‘(’ then TKlpar()
else if c = ‘)’ then (print(" rpar \n"); TKrpar())
//genes and booleans could share letters … need to fix
else if isBool1stCh© then
let
val logword = tostring© + whileCharTst(isBoolCh)
in //BEWARE of potential problems of function evaluation in situ…
if isAnd(string_caps(logword)) then TKand ()
else if isOr(string_caps(logword)) then TKor ()
else TKgene(logword) //Assume gene for now, e.g. could be "AND&|OR"
end
else if isGeneCh© then TKgene(tostring© + whileCharTst(isGeneCh))
//Build a string
else if isWhiteSpace© then
let
val _ = whileCharTst(isWhiteSpace)
in
getToken(ss)
end
else $raise UnforseenLexeme ()
end

(*
*)

Now that the segmentation fault is fixed, we see that there is still a bug
in the output:
The recaptured tokens don’t seem to be parsing the right parentheses
(TKrpar()):
Gene1 AND ( Gene2 r Gene3 OR Gene4 AND Gene5 END

Somehow we are getting an “r” where the “)” should be. Putting a space
between “Gene2” and the
following “)”, i.e.:
val teststr = "Gene1 AND (Gene2 ) or Gene3 or Gene4 and Gene5"
yields the following:

Gene1 AND ( Gene2 rpar
) r Gene3 OR Gene4 AND Gene5 END

So we are now finding the “)”, but we’re still getting the mystery “r”,
which at least isn’t so mysterious
anymore since we are missing the following “or”, and note that the original
"or" is lower case, unlike what
we get from the print_tks() function. Something in the tokenizer needs to
be fixed.

Using a “)” directly next to the “or”:
val teststr = "Gene1 AND (Gene2 )or Gene3 or Gene4 and Gene5"
we now correctly get:
Gene1 AND ( Gene2 rpar
) OR Gene3 OR Gene4 AND Gene5 END

So something is definitely happening with the whitespace consumption, but
why only here? At some point along the way I’d introduced a bug by
including an extra ss.next() in whileCharTst(). Removing it seems to fix
the problem. Now we have:

implement
getToken (ss: strstream): GRTOK =
let
val c = ss.peek()
val _ = ss.next(); //need to be fun1?
fun whileCharTst(chtst: char - bool): string =
let
val cw = ss.peek()
in
if chtst(cw) then
let
val _ = ss.next()
in
tostring(cw) + whileCharTst(chtst)
end
else “” //Need to theck next() stop condition
end
in
if c = ‘\000’ then TKEND()
else if c = ‘(’ then TKlpar()
else if c = ‘)’ then TKrpar()
//genes and booleans could share letters … need to fix
else if isBool1stCh© then
let
val logword = tostring© + whileCharTst(isBoolCh)
in
if isAnd(string_caps(logword)) then TKand ()
else if isOr(string_caps(logword)) then TKor ()
else TKgene(logword) //Assume gene for now, e.g. could be "AND&|OR"
end
else if isGeneCh© then TKgene(tostring© + whileCharTst(isGeneCh))
//Build a string
else if isWhiteSpace© then
let
val _ = whileCharTst(isWhiteSpace)
in
getToken(ss)
end
else $raise UnforseenLexeme ()
end

I think that your code suffers from the symptom I referred to as
"juggling-too-many-pieces-at-one-time" in my class.

WhileCharTst should not return a string. It just needs to stop at the
right place.
Another function should be implemented to extract the chars between two
given string
indices.

Also, strstream should be make abstract as you don’t yet know what methods
or functions
you really need to manipulate strstream.

val strtmp: string (apss) = mystr1_of_mystr(p->data + appstr)

This line cannot typecheck: the string on the right-hand side cannot be of
the type string(apss);
The string is actually of unknown length.

I am right now in the middle of something. I will try to implement sstream
for you to take a look later.

Can’t believe I missed that, thanks!

I forgot to mention, I am not using the same grammar as in the SML tutorial, but a Boolean expression (sans negation) grammar.

//
// HX-2013-02-20: Yes, I see a big improvement :slight_smile:
//
// I would suggest that sstream be implement as a record.
// This style would make it very easy if you later want to turn
// sstream into a linear abstype (absvtype).
//

local

typedef
sstream_struct (n:int) = @{
sstream_data= string (n)
, sstream_size= size_t (n)
, sstream_pos = sizeLte(n)
}
typedef sstream_struct = [n:nat] sstream_struct

assume sstream_type = ref (sstream_struct)

in (* in of [local] *)

implement
sstream_pos (ss) = let
val (vbox pf | p) = ref_get_view_ptr (ss) in p->pos
end // end of [sstream_pos]

end // end of [local]

Thanks, starting to get the idea here. I guess having a linear and and
functional alternative to sstream will be best.

A sstream is a stateful object. I think what you mean by a ‘functional
sstream’ is actually a so-called ‘persistent’ sstream, which is just a
reference to a sstream. A true functional sstream would need to create a
new copy once you call stream_inc (to increase the current position by
one). Try to compare a functional list with a (persistent) array in ML. A
true functional array is often implemented based on some form of tree (e.g.
a Braun tree); it is not a real array because a functional array does NOT
support O(1) subscripting functions.

Maybe I have confused you. What I really want to say here is that a
persistent sstream can be readily built on top of a linear one.

Unfortunately, as a library function, I don’t know how to give users the
power to choose between different string types without modifying the source
of one of the >>sstream.dats files.

Don’t worry about it now.

Then in general, I guess a string buffer is probably the best default to
use as an abstracted type. At least giving it an abstract interface would
allow us (or others) to later >>modify the code more easily if necessary,
which I believe is your point.

Yes, that is my point.

If all that sounds good I will work from there.

Have fun!

One more piece of advice: Don’t try to use dependent types at the
beginning. Try to finish a version based on non-dependent types. Only then,
you should, if you want to, think about how to use dependent types to
verify some properties (e.g., no array-bounds violations in this case).

Dependent types are kind of “flashy” or “sexy”. When programming in ATS,
people are often super eager to use them. What they probably don’t know but
will learn quickly is that they are almost surely to be burnt by dependent
types. This is why I keep saying that you have to have a program in order
to do program verification.

Cheers,

–Hongwei

Thanks! I fixed that particular issue, but more remain around it. There
are probably numerous other issues in the code I’d be grateful to hear
about before moving on to making a linear typed version and adding
dependent types.

Also if there are any missing functions for the sstream that you think are
general interest I’ll add them.

sstream_dats.txt (4.23 KB)

sstream_sats.txt (1.92 KB)

I believe I see one important problem here.

The sstream_struct has multiple members that are statically bound to the
integer n. Is there a synchronous way to update this all, or is it best to
create a new struct entirely? I attempted to use “and”, but I don’t know if
this is pratical or possible, as I’m still getting some errors. General
idea:

typedef
sstream_struct (n:int, m:int) = @{
data = mystring (n)
, size = size_t (n)
, pos = sizeLte (n)
, posmax = size_t (m)
}

typedef sstream_struct = [n,m:nat] sstream_struct (n, m)

// Inside write function:

val [d:int] ssz = size1_of_size(mystr_length(appstr))
val (vbox pf | p) = ref_get_view_ptr (ss)
val [n:int] n = size1_of_size(p->size)

// …
// synchronous static change
val _ = p->data := mystr1_of_mystr(p->data + appstr) and
_ = p->size := size1_of_size(p->size + ssz) and
_ = p->pos := p->pos: sizeLte(n+d)

Thanks for the suggestions; I may be using it incorrectly as the error
message remains largely the same, e.g.
prval [apss:int] INTEQ() = static_size(ssz+p->size)
val strtmp: string (apss) = mystr1_of_mystr(p->data + appstr)
val sztmp: size_t apss = p->size + ssz
val postmp: sizeLte(apss) = sizeLte_of_size1(p->pos, p->size + ssz)
val _ = p->data := strtmp
val _ = p->size := sztmp
val _ = p->pos := postmp

I also am a bit confused on the need for such a dataprop; we seem to be
showing x=x for x an integer, which is a (trivial) linear constraint, or am
I interpreting this wrong?

I forgot to say, the obvious solution would be to create a new strstream to
replace the old one, but this may not always be desirable.

Thank you for the hint; I got it to typecheck, still need to do more
testing.

Also, if you have time later, I’m very interested to hear about the
dataprop above.

Thanks! I have not implemented your suggestion yet, but I plan to. I went
ahead and implemented the parser, which was very easy to do (partly because
my grammar is isomorphic to that in the SML tutorial, and can’t get much
simpler anyway…). But also, I think I am past “the hard part” for now
(i.e. strings + dependent types newbie mistakes).

Though it passes typechecking, I get a “ccomp” error, and am not quite sure
what I’m doing wrong. I did see some other discussion related to this error
message on the mailing list, but it did not seem directly applicable.

/media/RAID5/share/ATS_learning/toCNF2.dats: 6385(line=268, offs=7) –
6540(line=270, offs=16): error(ccomp): the dynamic variable [Tzr] is not a
function argument.
exit(ATS): uncaught exception:
_2fhome_2ffac2_2fhwxi_2fresearch_2fATS_2fIMPLEMENT_2fGeizella_2fAnairiats_2fsvn_2fats_2dlang_2fsrc_2fats_error_2esats__FatalErrorException(1233)

toCNF2_dats.txt (6.96 KB)

To me, you don’t really need to learn linear logic in order to gain
understanding of linear types.
In ATS, the concept of linear types is quite intuitive and easy to
understand.

In practice, the primary difficulty comes from dependent types. In order to
use dependent types
effectively, one must have a solid understanding of (both universal and
existential) quantification.
I think any reasonable book on mathematical logic should help on this
matter.

static_size caused the problem. I think what you need is the following:

dataprop INTEQ (int, int) = {n:int} INTEQ (n, n)

prfun static_size1_size {n:nat} (s: !size_t n):<> [n2:int] INTEQ (n, n2)

Use it:

prval [n:int] INTEQ () = static_size (…)

Also, I was able to do it without the dataprop now: I see what was wrong,
but your dataprop was a hint. I needed to have a separate existential
variable that differed from the universal variable, i.e.,
prfun static_size1_size {n:nat} (s: !size_t n):<> [n2:nat | n2 == n] ()
//not:
//prfun static_size1_size {n:nat} (s: !size_t n):<> [n:nat] ()
//in this case, the two “n” variables are actually different, correct?

The error says that a value of type string (non-dependent) is used in a
place where a value of type String (dependent) is expected.

Thanks, starting to get the idea here. I guess having a linear and and
functional alternative to sstream will be best.

Unfortunately, as a library function, I don’t know how to give users the
power to choose between different string types without modifying the source
of one of the sstream.dats files.
Then in general, I guess a string buffer is probably the best default to
use as an abstracted type. At least giving it an abstract interface would
allow us (or others) to later modify the code
more easily if necessary, which I believe is your point.

If all that sounds good I will work from there.

Thanks,

Thanks for the clarification; I definitely misspoke about functional - what
I meant to say was GC-requiring (non-linear). For what it is worth, my
initial impression of dependent types was that the basic ideas (and syntax)
were easier to grasp in most cases than linear types, but this is exactly
what makes it dangerous (to one’s time). I’ve only used them when
necessary to deal with array (string) subscripting, I think.

Unfortunately, I didn’t quite make it through adding a new function without
running into some errors I don’t understand, though I guess it may be
something to do with the linearization of the reference (also out of all
the functions, I guess this one may have the worst style … ):

(* ****** ****** )
//
// Write (append) all characters from the stringr to the end
// of the sstream.
//
(
****** ****** *)

fun sstream_write(ss: sstream, appstr: string): void

implement
sstream_write(ss, appstr) = let
val ssz = mystr_length(appstr)
val (vbox pf | p) = ref_get_view_ptr (ss)
// Perform freeing before appending
// to avoid high memory use; likely only useful
// in strbuf version though.
val _ = (if p->posmax > 0 then let
val x = (if p->pos + ssz > p->posmax then let
val strtmp = mystr_substr(p->data, p->posmax, p->size)
val _ = p->data := strtmp
val _ = p->size := (p->size - p->pos + 1)
val _ = p->pos := (size1_of_int1(0))
in () end) // [end of if p->pos + ssz > p->posmax]
in () end) // [end of if p->posmax > 0]
val _ = p->data := p->data + appstr
val _ = p->size := p->size + ssz
in () end // end of [sstream_write]

-- mode: compilation; default-directory:
"/media/RAID5/share/ATS_learning/" -
-
Compilation started at Mon Apr 8 18:35:38

atscc -tc /media/RAID5/share/ATS_learning/sstream.dats
/media/RAID5/share/ATS_learning/ats-lang-anairiats-0.2.9/bin/atsopt
–typecheck --dynamic /media/RAID5/share/ATS_learning/sstream.dats
/media/RAID5/share/ATS_learning/sstream.dats: 3481(line=144, offs=47) –
3689(line=149, offs=14): error(3): mismatch of static terms (tyleq):
The needed term is: S2Eapp(S2Ecst(at_viewt0ype_addr_view);
S2Eexi(n$5860(9954); S2Eapp(S2Ecst(>=); S2Evar(n$5860(9954)), S2Eint(0));
S2Eapp(S2Ecst(sstream_struct); S2Evar(n$5860(9954)))),
S2Evar(l$5858$5859(9953)))
The actual term is: S2Eapp(S2Ecst(at_viewt0ype_addr_view); S2Etyrec(flt; 0;
data=S2Ecst(string_type), size=S2Eapp(S2Ecst(size_int_t0ype);
S2Eapp(S2Ecst(+); S2EVar(2547), S2EVar(2548))),
pos=S2Eapp(S2Ecst(size_int_t0ype); S2EVar(2549)), posmax=S2Ecst(size_t)),
S2Evar(l$5858$5859(9953)))
/media/RAID5/share/ATS_learning/sstream.dats: 3481(line=144, offs=47) –
3689(line=149, offs=14): error(3): mismatch of static terms (tyleq):
The needed term is: S2Eexi(n$5860(9954); S2Eapp(S2Ecst(>=);
S2Evar(n$5860(9954)), S2Eint(0)); S2Eapp(S2Ecst(sstream_struct);
S2Evar(n$5860(9954))))
The actual term is: S2Etyrec(flt; 0; data=S2Ecst(string_type),
size=S2Eapp(S2Ecst(size_int_t0ype); S2Eapp(S2Ecst(+); S2EVar(2547),
S2EVar(2548))), pos=S2Eapp(S2Ecst(size_int_t0ype); S2EVar(2549)),
posmax=S2Ecst(size_t))
/media/RAID5/share/ATS_learning/sstream.dats: 3481(line=144, offs=47) –
3689(line=149, offs=14): error(3): mismatch of static terms (tyleq):
The needed term is: S2Etyrec(flt; 0; data=S2Eapp(S2Ecst(mystring);
S2EVar(2552)), size=S2Eapp(S2Ecst(size_t); S2EVar(2552)),
pos=S2Eapp(S2Ecst(sizeLte); S2EVar(2552)), posmax=S2Ecst(size_t))
The actual term is: S2Etyrec(flt; 0; data=S2Ecst(string_type),
size=S2Eapp(S2Ecst(size_int_t0ype); S2Eapp(S2Ecst(+); S2EVar(2547),
S2EVar(2548))), pos=S2Eapp(S2Ecst(size_int_t0ype); S2EVar(2549)),
posmax=S2Ecst(size_t))
/media/RAID5/share/ATS_learning/sstream.dats: 3481(line=144, offs=47) –
3689(line=149, offs=14): error(3): mismatch of static terms (tyleq):
The needed term is: S2Eapp(S2Ecst(string_int_type); S2EVar(2552))
The actual term is: S2Ecst(string_type)
exit(ATS): uncaught exception:
_2fhome_2ffac2_2fhwxi_2fresearch_2fATS_2fIMPLEMENT_2fGeizella_2fAnairiats_2fsvn_2fats_2dlang_2fsrc_2fats_error_2esats__FatalErrorException(1233)

Compilation exited abnormally with code 1 at Mon Apr 8 18:35:38

Ah yes, I started reading this a few days ago. But before I go very far, do
you have any suggestions on a good introduction to linear logic (I don’t
even know what most of the symbols mean)?

In a simplified version of sstream_write, I get an error that may be
related to this at the end of the function:

/media/RAID5/share/ATS_learning/sstream.dats: 3371(line=141, offs=29) –
4308(line=164, offs=4): error(3): unsolved constraint: C3STRprop(main;
S2Eeqeq(S2Eapp(S2Ecst(add_int_int_int); S2EVar(2570), S2EVar(2571)),
S2Evar(n$5901(10004))))
/media/RAID5/share/ATS_learning/sstream.dats: 3371(line=141, offs=29) –
4308(line=164, offs=4): error(3): unsolved constraint for the dynamic
variable [pf]
type-checking has failed: there are some unsolved constraints: please
inspect the above reported error message(s) for information.

fun sstream_write(ss: sstream, appstr: string): void

symintr static_size
prfun static_size1_size {n:nat} (s: !size_t n):<> [n:nat] ()
overload static_size with static_size1_size

castfn sizeLte_of_size1 {n,m: nat | n <= m}
(n: !size_t n, m: !size_t m):<> sizeLte(m)

// end of relevant sstream.sats code

implement
sstream_write(ss, appstr) = let
val [d:int] ssz = size1_of_size(mystr_length(appstr))
val (vbox pf | p) = ref_get_view_ptr (ss)
val [n:int] _ = static_size(p->size)
val _ = p->data := mystr1_of_mystr(p->data + appstr) and
_ = p->size := p->size + ssz and
_ = p->pos := sizeLte_of_size1(p->pos, p->size + ssz)
in
()
end // end of [sstream_write]