# Some useful helpers for dealing with strings. recipe string-equal [ default-space:address:array:location <- new location:type, 30:literal a:address:array:character <- next-ingredient a-len:integer <- length a:address:array:character/deref b:address:array:character <- next-ingredient b-len:integer <- length b:address:array:character/deref # compare lengths { trace [string-equal], [comparing lengths] length-equal?:boolean <- equal a-len:integer, b-len:integer break-if length-equal?:boolean reply 0:literal } # compare each corresponding character trace [string-equal], [comparing characters] i:integer <- copy 0:literal { done?:boolean <- greater-or-equal i:integer, a-len:integer break-if done?:boolean a2:character <- index a:address:array:character/deref, i:integer b2:character <- index b:address:array:character/deref, i:integer { chars-match?:boolean <- equal a2:character, b2:character break-if chars-match?:boolean reply 0:literal } i:integer <- add i:integer, 1:literal loop } reply 1:literal ] scenario string-equal-reflexive [ run [ default-space:address:array:location <- new location:type, 30:literal x:address:array:character <- new [abc] 3:boolean/raw <- string-equal x:address:array:character, x:address:array:character ] memory should contain [ 3 <- 1 # x == x for all x ] ] scenario string-equal-identical [ run [ default-space:address:array:location <- new location:type, 30:literal x:address:array:character <- new [abc] y:address:array:character <- new [abc] 3:boolean/raw <- string-equal x:address:array:character, y:address:array:character ] memory should contain [ 3 <- 1 # abc == abc ] ] scenario string-equal-distinct-lengths [ run [ default-space:address:array:location <- new location:type, 30:literal x:address:array:character <- new [abc] y:address:array:character <- new [abcd] 3:boolean/raw <- string-equal x:address:array:character, y:address:array:character ] memory should contain [ 3 <- 0 # abc != abcd ] trace should contain [ string-equal: comparing lengths ] trace should not contain [ string-equal: comparing characters ] ] scenario string-equal-with-empty [ run [ default-space:address:array:location <- new location:type, 30:literal x:address:array:character <- new [] y:address:array:character <- new [abcd] 3:boolean/raw <- string-equal x:address:array:character, y:address:array:character ] memory should contain [ 3 <- 0 # "" != abcd ] ] scenario string-equal-common-lengths-but-distinct [ run [ default-space:address:array:location <- new location:type, 30:literal x:address:array:character <- new [abc] y:address:array:character <- new [abd] 3:boolean/raw <- string-equal x:address:array:character, y:address:array:character ] memory should contain [ 3 <- 0 # abc != abd ] ] # A new type to help incrementally construct strings. container buffer [ length:integer data:address:array:character ] recipe init-buffer [ default-space:address:array:location <- new location:type, 30:literal #? $print default-space:address:array:location #? $print [ #? ] result:address:buffer <- new buffer:type len:address:integer <- get-address result:address:buffer/deref, length:offset len:address:integer/deref <- copy 0:literal s:address:address:array:character <- get-address result:address:buffer/deref, data:offset capacity:integer <- next-ingredient s:address:address:array:character/deref <- new character:type, capacity:integer #? $print s:address:address:array:character/deref #? $print [ #? ] reply result:address:buffer ] recipe grow-buffer [ default-space:address:array:location <- new location:type, 30:literal in:address:buffer <- next-ingredient # double buffer size x:address:address:array:character <- get-address in:address:buffer/deref, data:offset oldlen:integer <- length x:address:address:array:character/deref/deref newlen:integer <- multiply oldlen:integer, 2:literal olddata:address:array:character <- copy x:address:address:array:character/deref x:address:address:array:character/deref <- new character:type, newlen:integer # copy old contents i:integer <- copy 0:literal { done?:boolean <- greater-or-equal i:integer, oldlen:integer break-if done?:boolean src:character <- index olddata:address:array:character/deref, i:integer dest:address:character <- index-address x:address:address:array:character/deref/deref, i:integer dest:address:character/deref <- copy src:character i:integer <- add i:integer, 1:literal loop } reply in:address:buffer ] recipe buffer-full? [ default-space:address:array:location <- new location:type, 30:literal in:address:buffer <- next-ingredient len:integer <- get in:address:buffer/deref, length:offset s:address:array:character <- get in:address:buffer/deref, data:offset capacity:integer <- length s:address:array:character/deref result:boolean <- greater-or-equal len:integer, capacity:integer reply result:boolean ] # in:address:buffer <- buffer-append in:address:buffer, c:character recipe buffer-append [ default-space:address:array:location <- new location:type, 30:literal in:address:buffer <- next-ingredient c:character <- next-ingredient { # grow buffer if necessary full?:boolean <- buffer-full? in:address:buffer break-unless full?:boolean in:address:buffer <- grow-buffer in:address:buffer } len:address:integer <- get-address in:address:buffer/deref, length:offset s:address:array:character <- get in:address:buffer/deref, data:offset dest:address:character <- index-address s:address:array:character/deref, len:address:integer/deref dest:address:character/deref <- copy c:character len:address:integer/deref <- add len:address:integer/deref, 1:literal reply in:address:buffer/same-as-arg:0 ] scenario buffer-append-works [ run [ default-space:address:array:location <- new location:type, 30:literal x:address:buffer <- init-buffer 3:literal s1:address:array:character <- get x:address:buffer/deref, data:offset x:address:buffer <- buffer-append x:address:buffer, 97:literal # 'a' x:address:buffer <- buffer-append x:address:buffer, 98:literal # 'b' x:address:buffer <- buffer-append x:address:buffer, 99:literal # 'c' s2:address:array:character <- get x:address:buffer/deref, data:offset 1:boolean/raw <- equal s1:address:array:character, s2:address:array:character #? $print s2:address:array:character #? $print [ #? ] #? $print 1060:integer/raw #? $print [ #? ] #? $print 1061:integer/raw #? $print [ #? ] #? $print 1062:integer/raw #? $print [ #? ] #? $print 1063:integer/raw #? $print [ #? ] #? $print 1064:integer/raw #? $print [ #? ] #? $print 1065:integer/raw #? $print [ #? ] 2:array:character/raw <- copy s2:address:array:character/deref +buffer-filled x:address:buffer <- buffer-append x:address:buffer, 100:literal # 'd' s3:address:array:character <- get x:address:buffer/deref, data:offset 10:boolean/raw <- equal s1:address:array:character, s3:address:array:character 11:integer/raw <- get x:address:buffer/deref, length:offset 12:array:character/raw <- copy s3:address:array:character/deref ] memory should contain [ # before +buffer-filled 1 <- 1 # no change in data pointer 2 <- 3 # size of data 3 <- 97 # data 4 <- 98 5 <- 99 # in the end 10 <- 0 # data pointer has grown 11 <- 4 # final length 12 <- 6 # but data's capacity has doubled 13 <- 97 # data 14 <- 98 15 <- 99 16 <- 100 17 <- 0 18 <- 0 ] ] # result:address:array:character <- integer-to-decimal-string n:integer recipe integer-to-decimal-string [ default-space:address:array:location <- new location:type, 30:literal n:integer <- next-ingredient # is it zero? { break-if n:integer result:address:array:character <- new [0] reply result:address:array:character } # save sign negate-result:boolean <- copy 0:literal { negative?:boolean <- lesser-than n:integer, 0:literal break-unless negative?:boolean negate-result:boolean <- copy 1:literal n:integer <- multiply n:integer, -1:literal } # add digits from right to left into intermediate buffer tmp:address:buffer <- init-buffer 30:literal digit-base:integer <- copy 48:literal # '0' { done?:boolean <- equal n:integer, 0:literal break-if done?:boolean n:integer, digit:integer <- divide-with-remainder n:integer, 10:literal c:character <- add digit-base:integer, digit:integer tmp:address:buffer <- buffer-append tmp:address:buffer, c:character loop } # add sign { break-unless negate-result:boolean tmp:address:buffer <- buffer-append tmp:address:buffer, 45:literal # '-' } # reverse buffer into string result len:integer <- get tmp:address:buffer/deref, length:offset buf:address:array:character <- get tmp:address:buffer/deref, data:offset result:address:array:character <- new character:type, len:integer i:integer <- subtract len:integer, 1:literal j:integer <- copy 0:literal { # while i >= 0 done?:boolean <- lesser-than i:integer, 0:literal break-if done?:boolean # result[j] = tmp[i] src:character <- index buf:address:array:character/deref, i:integer dest:address:character <- index-address result:address:array:character/deref, j:integer dest:address:character/deref <- copy src:character # ++i i:integer <- subtract i:integer, 1:literal # --j j:integer <- add j:integer, 1:literal loop } reply result:address:array:character ] scenario integer-to-decimal-digit-zero [ run [ 1:address:array:character/raw <- integer-to-decimal-string 0:literal 2:array:character/raw <- copy 1:address:array:character/deref/raw ] memory should contain [ 2:string <- [0] ] ] scenario integer-to-decimal-digit-positive [ run [ 1:address:array:character/raw <- integer-to-decimal-string 234:literal 2:array:character/raw <- copy 1:address:array:character/deref/raw ] memory should contain [ 2:string <- [234] ] ] scenario integer-to-decimal-digit-negative [ run [ 1:address:array:character/raw <- integer-to-decimal-string -1:literal 2:array:character/raw <- copy 1:address:array:character/deref/raw ] memory should contain [ 2 <- 2 3 <- 45 # '-' 4 <- 49 # '1' ] ] recipe string-append [ default-space:address:array:location <- new location:type, 30:literal # result = new string[a.length + b.length] a:address:array:character <- next-ingredient a-len:integer <- length a:address:array:character/deref b:address:array:character <- next-ingredient b-len:integer <- length b:address:array:character/deref result-len:integer <- add a-len:integer, b-len:integer result:address:array:character <- new character:type, result-len:integer # copy a into result result-idx:integer <- copy 0:literal i:integer <- copy 0:literal { # while i < a.length a-done?:boolean <- greater-or-equal i:integer, a-len:integer break-if a-done?:boolean # result[result-idx] = a[i] out:address:character <- index-address result:address:array:character/deref, result-idx:integer in:character <- index a:address:array:character/deref, i:integer out:address:character/deref <- copy in:character # ++i i:integer <- add i:integer, 1:literal # ++result-idx result-idx:integer <- add result-idx:integer, 1:literal loop } # copy b into result i:integer <- copy 0:literal { # while i < b.length b-done?:boolean <- greater-or-equal i:integer, b-len:integer break-if b-done?:boolean # result[result-idx] = a[i] out:address:character <- index-address result:address:array:character/deref, result-idx:integer in:character <- index b:address:array:character/deref, i:integer out:address:character/deref <- copy in:character # ++i i:integer <- add i:integer, 1:literal # ++result-idx result-idx:integer <- add result-idx:integer, 1:literal loop } reply result:address:array:character ] scenario string-append-1 [ run [ 1:address:array:character/raw <- new [hello,] 2:address:array:character/raw <- new [ world!] 3:address:array:character/raw <- string-append 1:address:array:character/raw, 2:address:array:character/raw 4:array:character/raw <- copy 3:address:array:character/raw/deref ] memory should contain [ 4:string <- [hello, world!] ] ] # replace underscores in first with remaining args # result:address:array:character <- interpolate template:address:array:character, ... recipe interpolate [ default-space:array:address:location <- new location:type, 60:literal template:address:array:character <- next-ingredient # compute result-len, space to allocate for result tem-len:integer <- length template:address:array:character/deref result-len:integer <- copy tem-len:integer { # while arg received a:address:array:character, arg-received?:boolean <- next-ingredient break-unless arg-received?:boolean # result-len = result-len + arg.length - 1 for the 'underscore' being replaced a-len:integer <- length a:address:array:character/deref result-len:integer <- add result-len:integer, a-len:integer result-len:integer <- subtract result-len:integer, 1:literal loop } #? $print tem-len:integer #? 1 #? $print [ ] #? 1 #? $print result-len:integer #? 1 #? $print [ #? 1 #? ] #? 1 rewind-ingredients _ <- next-ingredient # skip template # result = new array:character[result-len] result:address:array:character <- new character:type, result-len:integer # repeatedly copy sections of template and 'holes' into result result-idx:integer <- copy 0:literal i:integer <- copy 0:literal { # while arg received a:address:array:character, arg-received?:boolean <- next-ingredient break-unless arg-received?:boolean # copy template into result until '_' { # while i < template.length tem-done?:boolean <- greater-or-equal i:integer, tem-len:integer break-if tem-done?:boolean, 2:blocks # while template[i] != '_' in:character <- index template:address:array:character/deref, i:integer underscore?:boolean <- equal in:character, 95:literal # '_' break-if underscore?:boolean # result[result-idx] = template[i] out:address:character <- index-address result:address:array:character/deref, result-idx:integer out:address:character/deref <- copy in:character # ++i i:integer <- add i:integer, 1:literal # ++result-idx result-idx:integer <- add result-idx:integer, 1:literal loop } # copy 'a' into result j:integer <- copy 0:literal { # while j < a.length arg-done?:boolean <- greater-or-equal j:integer, a-len:integer break-if arg-done?:boolean # result[result-idx] = a[j] in:character <- index a:address:array:character/deref, j:integer out:address:character <- index-address result:address:array:character/deref, result-idx:integer out:address:character/deref <- copy in:character # ++j j:integer <- add j:integer, 1:literal # ++result-idx result-idx:integer <- add result-idx:integer, 1:literal loop } # skip '_' in template i:integer <- add i:integer, 1:literal loop # interpolate next arg } # done with holes; copy rest of template directly into result { # while i < template.length tem-done?:boolean <- greater-or-equal i:integer, tem-len:integer break-if tem-done?:boolean # result[result-idx] = template[i] in:character <- index template:address:array:character/deref, i:integer out:address:character <- index-address result:address:array:character/deref, result-idx:integer out:address:character/deref <- copy in:character # ++i i:integer <- add i:integer, 1:literal # ++result-idx result-idx:integer <- add result-idx:integer, 1:literal loop } reply result:address:array:character ] scenario interpolate-works [ #? dump run #? 1 run [ 1:address:array:character/raw <- new [abc _] 2:address:array:character/raw <- new [def] 3:address:array:character/raw <- interpolate 1:address:array:character/raw, 2:address:array:character/raw 4:array:character/raw <- copy 3:address:array:character/raw/deref ] memory should contain [ 4:string <- [abc def] ] ] scenario interpolate-at-start [ run [ 1:address:array:character/raw <- new [_, hello!] 2:address:array:character/raw <- new [abc] 3:address:array:character/raw <- interpolate 1:address:array:character/raw, 2:address:array:character/raw 4:array:character/raw <- copy 3:address:array:character/raw/deref ] memory should contain [ 4:string <- [abc, hello!] 16 <- 0 # out of bounds ] ] scenario interpolate-at-end [ run [ 1:address:array:character/raw <- new [hello, _] 2:address:array:character/raw <- new [abc] 3:address:array:character/raw <- interpolate 1:address:array:character/raw, 2:address:array:character/raw 4:array:character/raw <- copy 3:address:array:character/raw/deref ] memory should contain [ 4:string <- [hello, abc] ] ]