518 lines
21 KiB
NASM
518 lines
21 KiB
NASM
; =====> SSTRING.ASM
|
||
;************************************************************************
|
||
;* TIPC Scheme Runtime Support *
|
||
;* Interpreter -- String Operations *
|
||
;* *
|
||
;* (C) Copyright 1985 by Texas Instruments Incorporated. *
|
||
;* All rights reserved. *
|
||
;* *
|
||
;* Date Written: 18 January 1985 *
|
||
;* Last Modification: *
|
||
;* 4/27/88 (tc) - removed case conversion from characters in the range *
|
||
;* of 128 through 167 (see locases, hicases, collate). *
|
||
;* Our previous assumptions did not work for some inter-*
|
||
;* national character sets. *
|
||
;************************************************************************
|
||
include scheme.equ
|
||
include sinterp.mac
|
||
|
||
include sinterp.arg
|
||
|
||
DGROUP group data
|
||
data segment word public 'DATA'
|
||
assume DS:DGROUP
|
||
|
||
; Local data definitions
|
||
m_ch_eq db "CHAR=?",0
|
||
m_ceq_ci db "CHAR-CI=?",0
|
||
m_ch_lt db "CHAR<?",0
|
||
m_chl_ci db "CHAR-CI<?",0
|
||
m_ch_up db "CHAR-UPCASE",0
|
||
m_ch_dwn db "CHAR-DOWNCASE",0
|
||
m_mk_str db "MAKE-STRING",0
|
||
m_st_fl db "FILL-STRING!",0
|
||
m_st_ref db "STRING-REF",0
|
||
m_st_set db "STRING-SET!",0
|
||
m_one dw 1 ; a constant "one" (1)
|
||
m_two dw 2 ; a constant "two" (2)
|
||
m_soff dw STRING_OFFSET_ERROR ; error code
|
||
|
||
; Case tables (for characters between 40h and 0bfh)
|
||
public locases,hicases,collate
|
||
|
||
locases db 000,001,002,003,004,005,006,007
|
||
db 008,009,010,011,012,013,014,015
|
||
db 016,017,018,019,020,021,022,023
|
||
db 024,025,026,027,028,029,030,031
|
||
db " ","!",'"',"#","$","%","&","'"
|
||
db "(",")","*","+",",","-",".","/"
|
||
db "0","1","2","3","4","5","6","7"
|
||
db "8","9",":",";","<","=",">","?"
|
||
|
||
db "@","a","b","c","d","e","f","g"
|
||
db "h","i","j","k","l","m","n","o"
|
||
db "p","q","r","s","t","u","v","w"
|
||
db "x","y","z","[","\","]","^","_"
|
||
db "`","a","b","c","d","e","f","g"
|
||
db "h","i","j","k","l","m","n","o"
|
||
db "p","q","r","s","t","u","v","w"
|
||
db "x","y","z","{","|","}","~",127
|
||
; C .. ' ^ .. ` o c
|
||
; ' u e a a a a '
|
||
db 128,129,130,131,132,133,134,135 ;135,129,130,131,132,133,134,135
|
||
; ^ .. ` .. ^ ` .. o
|
||
; e e e i i i A A
|
||
db 136,137,138,139,140,141,142,143 ;136,137,138,139,140,141,132,134
|
||
; ' ^ .. ` ^ `
|
||
; E ae AE o o o u u
|
||
db 144,145,146,147,148,149,150,151 ;130,145,145,147,148,149,150,151
|
||
; .. .. ..
|
||
; y O U (currency symbols)
|
||
db 152,153,154,155,156,157,158,159 ;152,148,129,155,156,157,158,159
|
||
; ' ' ' ' ~ ~
|
||
; a i o u n N
|
||
db 160,161,162,163,164,165,166,167 ;160,161,162,163,164,164,166,167
|
||
db 168,169,170,171,172,173,174,175
|
||
db 176,177,178,179,180,181,182,183
|
||
db 184,185,186,187,188,189,190,191
|
||
|
||
db 192,193,194,195,196,197,198,199
|
||
db 200,201,202,203,204,205,206,207
|
||
db 208,209,210,211,212,213,214,215
|
||
db 216,217,218,219,220,221,222,223
|
||
; beta
|
||
db 224,225,226,227,228,229,230,231
|
||
db 232,233,234,235,236,237,238,239
|
||
db 240,241,242,243,244,245,246,247
|
||
db 248,249,250,251,252,253,254,255
|
||
|
||
hicases db 000,001,002,003,004,005,006,007
|
||
db 008,009,010,011,012,013,014,015
|
||
db 016,017,018,019,020,021,022,023
|
||
db 024,025,026,027,028,029,030,031
|
||
db " ","!",'"',"#","$","%","&","'"
|
||
db "(",")","*","+",",","-",".","/"
|
||
db "0","1","2","3","4","5","6","7"
|
||
db "8","9",":",";","<","=",">","?"
|
||
|
||
db "@","A","B","C","D","E","F","G"
|
||
db "H","I","J","K","L","M","N","O"
|
||
db "P","Q","R","S","T","U","V","W"
|
||
db "X","Y","Z","[","\","]","^","_"
|
||
db "`","A","B","C","D","E","F","G"
|
||
db "H","I","J","K","L","M","N","O"
|
||
db "P","Q","R","S","T","U","V","W"
|
||
db "X","Y","Z","{","|","}","~",127
|
||
; C .. ' ^ .. ` o c
|
||
; ' u e a a a a '
|
||
db 128,129,130,131,132,133,134,135 ;128,154,144,"A",142,"A",143,128
|
||
; ^ .. ` .. ^ ` .. o
|
||
; e e e i i i A A
|
||
db 136,137,138,139,140,141,142,143 ;"E","E","E","I","I","I",142,143
|
||
; ' ^ .. ` ^ `
|
||
; E ae AE o o o u u
|
||
db 144,145,146,147,148,149,150,151 ;144,146,146,"O",153,"O","U","U"
|
||
;.. .. ..
|
||
; y O U (currency symbols)
|
||
db 152,153,154,155,156,157,158,159 ;"Y",153,154,155,156,157,158,159
|
||
; ' ' ' ' ~ ~
|
||
; a i o u n N
|
||
db 160,161,162,163,164,165,166,167 ;"A","I","O","U",165,165,166,167
|
||
db 168,169,170,171,172,173,174,175
|
||
db 176,177,178,179,180,181,182,183
|
||
db 184,185,186,187,188,189,190,191
|
||
|
||
db 192,193,194,195,196,197,198,199
|
||
db 200,201,202,203,204,205,206,207
|
||
db 208,209,210,211,212,213,214,215
|
||
db 216,217,218,219,220,221,222,223
|
||
; beta
|
||
db 224,225,226,227,228,229,230,231
|
||
db 232,233,234,235,236,237,238,239
|
||
db 240,241,242,243,244,245,246,247
|
||
db 248,249,250,251,252,253,254,255
|
||
|
||
collate db 000,001,002,003,004,005,006,007
|
||
db 008,009,010,011,012,013,014,015
|
||
db 016,017,018,019,020,021,022,023
|
||
db 024,025,026,027,028,029,030,031
|
||
db " ","!",'"',"#","$","%","&","'"
|
||
db "(",")","*","+",",","-",".","/"
|
||
db "0","1","2","3","4","5","6","7"
|
||
db "8","9",":",";","<","=",">","?"
|
||
|
||
db "@","A","B","C","D","E","F","G"
|
||
db "H","I","J","K","L","M","N","O"
|
||
db "P","Q","R","S","T","U","V","W"
|
||
db "X","Y","Z","[","\","]","^","_"
|
||
db "`","a","b","c","d","e","f","g"
|
||
db "h","i","j","k","l","m","n","o"
|
||
db "p","q","r","s","t","u","v","w"
|
||
db "x","y","z","{","|","}","~",127
|
||
; C .. ' ^ .. ` o c
|
||
; ' u e a a a a '
|
||
db 128,129,130,131,132,133,134,135 ;"C","u","e","a","a","a","a","c"
|
||
; ^ .. ` .. ^ ` .. o
|
||
; e e e i i i A A
|
||
db 136,137,138,139,140,141,142,143 ;"e","e","e","i","i","i","A","A"
|
||
; ' ^ .. ` ^ `
|
||
; E ae AE o o o u u
|
||
db 144,145,146,147,148,149,150,151 ;"E","a","A","o","o","o","u","u"
|
||
; .. .. ..
|
||
;y O U (currency symbols)
|
||
db 152,153,154,155,156,157,158,159 ;"y","O","U","$","$","$","$","$"
|
||
; ' ' ' ' ~ ~
|
||
; a i o u n N
|
||
db 160,161,162,163,164,165,166,177 ;"a","i","o","u","n","N",166,167
|
||
db 168,169,170,171,172,173,174,175 ;"?",169,170,171,172,"!",'"','"'
|
||
db 176,177,178,179,180,181,182,183
|
||
db 184,185,186,187,188,189,190,191
|
||
|
||
db 192,193,194,195,196,197,198,199
|
||
db 200,201,202,203,204,205,206,207
|
||
db 208,209,210,211,212,213,214,215
|
||
db 216,217,218,219,220,221,222,223
|
||
; beta
|
||
db 224,"s",226,227,228,229,230,231
|
||
db 232,233,234,235,236,237,238,239
|
||
db 240,241,242,243,244,245,246,247
|
||
db 248,249,250,251,252,253,254,255
|
||
|
||
data ends
|
||
|
||
|
||
PGROUP group prog
|
||
prog segment byte public 'PROG'
|
||
assume CS:PGROUP
|
||
|
||
str_int proc near
|
||
|
||
; Entry points defined in "sinterp.asm"
|
||
extrn next:near ; Top of interpreter
|
||
extrn next_PC:near ; Reload ES,SI at top of interpreter
|
||
extrn next_SP:near ; mov SP,BP before next_PC
|
||
extrn src_err:near ; Source (operand) error print routine
|
||
extrn sch_err:near ; link to Scheme debugger
|
||
|
||
char_cmp macro comparison,case,operation
|
||
local w,x,y,z
|
||
lods word ptr ES:[SI] ; load operands
|
||
xor BX,BX
|
||
mov BL,AL ; copy the destination=source1 register
|
||
mov DI,BX ; number, copy into DI, and compute
|
||
add DI,offset reg0 ; the register's address
|
||
mov BL,AH ; copy the source2 register number
|
||
mov AL,byte ptr reg0_pag+[BX] ; load tag of src2 operand
|
||
cmp AL,SPECCHAR*2 ; is source2 a character?
|
||
jne z ; if not, error (jump)
|
||
cmp AL,byte ptr [DI].C_page ; is source1 a character?
|
||
jne z ; if not, error (jump)
|
||
IFIDN <case>,<CI>
|
||
mov AL,byte ptr reg0_dis+[BX] ; move character value of source2
|
||
mov BX,offset locases ; Fetch lower-case equivalents
|
||
xlat
|
||
mov AH,AL ; Save equivalent in AH
|
||
mov AL,byte ptr[DI].C_disp ; move char value of source1
|
||
xlat ; Fetch lower-case eqivalent
|
||
ELSE
|
||
mov AL,byte ptr [DI].C_disp ; Fetch characters
|
||
mov AH,byte ptr reg0_dis+[BX]
|
||
ENDIF
|
||
mov BX,offset collate ; Get collation values of chars
|
||
xlat
|
||
xchg AL,AH
|
||
xlat
|
||
cmp AH,AL ; Compare
|
||
j&comparison y ; jump, if test is satisfied
|
||
xor AX,AX ; place 'nil in destination
|
||
mov byte ptr [DI].C_page,AL ; register
|
||
mov [DI].C_disp,AX
|
||
jmp next ; return to interpreter
|
||
y: mov byte ptr [DI].C_page,T_PAGE*2 ; place 't in
|
||
mov [DI].C_disp,T_DISP ; destination register
|
||
jmp next ; return to interpreter
|
||
; ***error-- one (or both) operands aren't characters***
|
||
z: mov AX,offset operation
|
||
IFIDN <operation>,<m_ch_eq>
|
||
error_2: add BX,offset reg0 ; compute address of source 2
|
||
pushm <BX,DI,m_two,AX> ; push source 2, source 1, operation name
|
||
C_call set_src_,<SI>,Load_ES ; call: set_source_error
|
||
jmp sch_err ; link to Scheme debugger
|
||
ELSE
|
||
jmp error_2
|
||
ENDIF
|
||
endm
|
||
|
||
|
||
;************************************************************************
|
||
;* AL AH *
|
||
;* (char-= char1 char2) char-= dest,src *
|
||
;* *
|
||
;* Purpose: Scheme interpreter support for comparing the equality of *
|
||
;* character data objects. *
|
||
;* *
|
||
;* Description: The tags (page numbers) or the objects are compared *
|
||
;* for equality. If they are not equal, at least one of *
|
||
;* the operands is not a character, and an error is *
|
||
;* signaled. If they are equal, a second check to make *
|
||
;* sure both are characters is performed. *
|
||
;* *
|
||
;* After validating the tag fields, the displacement fields*
|
||
;* are compared for equality. If they are identical, the *
|
||
;* characters are equal and 't is returned in the destina- *
|
||
;* tion register. If not equal, 'nil is returned in the *
|
||
;* destination register. *
|
||
;************************************************************************
|
||
public ch_eq_p
|
||
ch_eq_p: char_cmp e,CS,m_ch_eq
|
||
|
||
;************************************************************************
|
||
;* AL AH *
|
||
;* (char-equal? char1 char2) char-eq? dest,src *
|
||
;* *
|
||
;* Purpose: Scheme interpreter support for comparing the equality of *
|
||
;* character data objects ignoring case. *
|
||
;* *
|
||
;* Description: The tags (page numbers) or the objects are compared *
|
||
;* for equality. If they are not equal, at least one of *
|
||
;* the operands is not a character, and an error is *
|
||
;* signaled. If they are equal, a second check to make *
|
||
;* sure both are characters is performed. *
|
||
;* *
|
||
;* The displacements of both operands are loaded and *
|
||
;* mapped to uppercase. They are then compared for *
|
||
;* equality. If equal, 't is returned in the destination *
|
||
;* registers. Otherwise, 'nil is returned. *
|
||
;************************************************************************
|
||
public ch_eq_ci
|
||
ch_eq_ci: char_cmp e,CI,m_ceq_ci
|
||
|
||
;************************************************************************
|
||
;* AL AH *
|
||
;* (char-<char1 char2) char-< dest,src *
|
||
;************************************************************************
|
||
public ch_lt_p
|
||
ch_lt_p: char_cmp b,CS,m_ch_lt
|
||
|
||
;************************************************************************
|
||
;* AL AH *
|
||
;* (char-less? char1 char2) char-less? dest,src *
|
||
;************************************************************************
|
||
public ch_lt_ci
|
||
ch_lt_ci: char_cmp b,CI,m_chl_ci
|
||
|
||
purge char_cmp
|
||
|
||
ch_case macro direction,name
|
||
local y
|
||
lods byte ptr ES:[SI]
|
||
mov DI,AX
|
||
add DI,offset reg0
|
||
cmp byte ptr [DI].C_page,SPECCHAR*2 ; is input char?
|
||
jne y ; if not a character, error (jump)
|
||
mov AL,byte ptr [DI].C_disp ; Put char in AL
|
||
IFIDN <direction>,<UP>
|
||
mov BX,offset hicases
|
||
ELSE
|
||
IFIDN <direction>,<DOWN>
|
||
mov BX,offset locases
|
||
ELSE
|
||
***error*** Invalid: direction
|
||
ENDIF
|
||
ENDIF
|
||
xlat ; Fetch alternate case
|
||
mov byte ptr [DI].C_disp,AL ; and store into dest register
|
||
jmp next
|
||
; ***error-- invalid source operand***
|
||
y: mov AX,offset name ; load the instruction's name text
|
||
IFIDN <direction>,<UP>
|
||
error_1: pushm <DI,m_one,AX> ; push operand, operand count, instr. name
|
||
C_call set_src_,<SI>,Load_ES ; call set_source_error
|
||
jmp sch_err ; link to Scheme debugger
|
||
ELSE
|
||
jmp error_1 ; jump to error routine
|
||
ENDIF
|
||
endm
|
||
|
||
;************************************************************************
|
||
;* AL *
|
||
;* (char-upcase char) char-upcase dest *
|
||
;* *
|
||
;* Purpose: Scheme interpreter support for conversion of characters *
|
||
;* to uppercase *
|
||
;************************************************************************
|
||
public ch_up
|
||
ch_up: ch_case UP,m_ch_up
|
||
|
||
;************************************************************************
|
||
;* AL *
|
||
;* (char-downcase char) char-downcase dest *
|
||
;* *
|
||
;* Purpose: Scheme interpreter support for conversion of characters *
|
||
;* to lowercase *
|
||
;************************************************************************
|
||
public ch_down
|
||
ch_down: ch_case DOWN,m_ch_dwn
|
||
|
||
purge ch_case
|
||
|
||
;************************************************************************
|
||
;* AL AH *
|
||
;* (make-string len init) make-string len,init*
|
||
;************************************************************************
|
||
public make_str
|
||
make_str: lods word ptr ES:[SI] ; load the operands of the instruction
|
||
save <AX,SI> ; save the operands and location pointer
|
||
xor BX,BX
|
||
mov BL,AL ; copy the destination register number
|
||
add BX,offset reg0 ; into BX and compute its address
|
||
cmp byte ptr [BX].C_page,SPECFIX*2 ; is length a fixnum?
|
||
jne mk_st_er ; if not, error (jump)
|
||
mov AX,[BX].C_disp ; load the immediate value for the size
|
||
shl AX,1 ; and sign extend it
|
||
sar AX,1
|
||
jl mk_st_er ; if size is negative, error
|
||
mov CX,STRTYPE ; load the tag value for the string object
|
||
pushm <AX,CX,BX> ; push arguments to "alloc_block"
|
||
C_call alloc_bl,,Load_ES ; call: alloc_block(reg, STRTYPE, len)
|
||
pop BX ; restore destination register address
|
||
mov DI,[BX].C_disp ; load a pointer to the newly allocated
|
||
mov BX,[BX].C_page ; string object
|
||
LoadPage ES,BX
|
||
;;; mov ES,pagetabl+[BX]
|
||
restore <AX> ; reload operands to instruction
|
||
mov BL,AH ; copy initial value register number
|
||
mov AL,byte ptr reg0_pag+[BX] ; load page number of init value
|
||
cmp AL,SPECCHAR*2 ; init value a character?
|
||
je st_fl_3 ; if a character, jump
|
||
cmp AL,NIL_PAGE*2 ; use default value? (nil?)
|
||
jne mk_st_er ; if not nil, error (jump)
|
||
mov AL," " ; load default string fill character
|
||
jmp short st_fl_4
|
||
mk_st_er: lea BX,m_mk_str ; load address of "make-string" text
|
||
jmp src_err ; display "source operand error" message
|
||
|
||
;************************************************************************
|
||
;* AL AH *
|
||
;* (string-fill! string char) string-fill! str,char *
|
||
;************************************************************************
|
||
public str_fill
|
||
str_fill: lods word ptr ES:[SI] ; load string-fill operands
|
||
save <SI> ; save current location pointer
|
||
xor BX,BX
|
||
mov BL,AL ; copy string register number
|
||
mov DI,BX
|
||
mov BL,byte ptr reg0_pag+[DI]
|
||
cmp byte ptr ptype+[BX],STRTYPE*2 ; is 1st operand a string?
|
||
jne st_fl_er ; if not, error (jump)
|
||
LoadPage ES,BX
|
||
;;; mov ES,pagetabl+[BX] ; load a pointer to the string object
|
||
mov DI,reg0_dis+[DI]
|
||
mov BL,AH ; copy initialization value register number
|
||
cmp reg0_pag+[BX],SPECCHAR*2 ; is it a char?
|
||
jne st_fl_er ; if not, error
|
||
st_fl_3: mov AL,byte ptr reg0_dis+[BX] ; load initialization character
|
||
st_fl_4: mov CX,ES:[DI].str_len ; load length of string object
|
||
cmp CX,0 ;;; check for small string
|
||
jge st_010
|
||
add CX,BLK_OVHD+PTRSIZE ;;; adjust for small string
|
||
st_010: sub CX,offset str_data ; compute number of characters
|
||
add DI,offset str_data ; advance index to 1st character position
|
||
rep stosb ; fill string object with init character
|
||
jmp next_SP ; return to interpreter
|
||
st_fl_er: lea BX,m_st_fl ; load address of "fill-string" text
|
||
jmp src_err ; display "source operand error" message
|
||
|
||
str_int endp
|
||
|
||
;************************************************************************
|
||
;* Macro Support for String ref/set *
|
||
;************************************************************************
|
||
st_thing macro ref_or_set,message
|
||
local w,x,y,z
|
||
lods word ptr ES:[SI] ; load string pointer and index regs
|
||
xor BX,BX
|
||
mov BL,AL ; copy string/dest reg number into DI
|
||
mov DI,BX
|
||
IFIDN <ref_or_set>,<SET>
|
||
lods byte ptr ES:[SI] ; load source operand register number
|
||
mov DL,AL ; and save it in TIPC register DL
|
||
ENDIF
|
||
save <SI> ; save the location pointer
|
||
mov BL,byte ptr reg0_pag+[DI] ; load string page number
|
||
cmp byte ptr ptype+[BX],STRTYPE*2 ; is it a string?
|
||
jne y ; if not a string, error (jump)
|
||
LoadPage ES,BX
|
||
;;; mov ES,pagetabl+[BX]
|
||
mov BL,AH ; copy index register number
|
||
cmp byte ptr reg0_pag+[BX],SPECFIX*2 ; is index a fixnum?
|
||
jne y ; if not a fixnum, error (jump)
|
||
mov AX,reg0_dis+[BX] ; load immediate value and
|
||
shl AX,1 ; sign extend to 16 bits
|
||
sar AX,1
|
||
jl z ; if index negative, error (jump)
|
||
add AX,offset str_data ; advance pointer past string header
|
||
mov SI,reg0_dis+[DI] ; load pointer to string object
|
||
mov CX,ES:[SI].str_len ; compare index with string length
|
||
cmp CX,0 ;;; check for small string
|
||
jge w
|
||
add CX,BLK_OVHD+PTRSIZE ;;; adjust for small string
|
||
w: cmp AX,CX
|
||
jge z ; if index too large, error (jump)
|
||
add SI,AX ; add index to string pointer
|
||
IFIDN <ref_or_set>,<REF>
|
||
mov byte ptr reg0_pag+[DI],SPECCHAR*2 ; set tag=character
|
||
mov BL,ES:[SI] ; fetch desired character
|
||
mov reg0_dis+[DI],BX ; and store into destination register
|
||
ELSE
|
||
IFIDN <ref_or_set>,<SET>
|
||
mov BL,DL ; copy source value register number
|
||
cmp byte ptr reg0_pag+[BX],SPECCHAR*2 ; is source a character?
|
||
jne y ; if not a character, error (jump)
|
||
mov AL,byte ptr reg0_dis+[BX] ; store source character into
|
||
mov ES:[SI],AL ; string at desired offset
|
||
ELSE
|
||
***error*** Invalid: ref_or_set
|
||
ENDIF
|
||
ENDIF
|
||
jmp next_PC ; return to interpreter
|
||
; ***error-- invalid source operand***
|
||
y: lea BX,message ; load address of operation name text
|
||
jmp src_err ; display "source operand error" message
|
||
; ***error-- invalid string offset***
|
||
z: mov BX,offset message ; load address of instruction name
|
||
IFIDN <ref_or_set>,<REF>
|
||
mov DX,3 ; STRING-REF is 3 bytes long
|
||
s_out_bn: restore <SI> ; load location pointer and
|
||
sub SI,DX ; back up to start of instruction in error
|
||
pushm <SI,BX> ; push instruction's offset, name
|
||
C_call disassem,,Load_ES ; disassemble instruction for *irritant*
|
||
pushm <tmp_adr,m_soff,m_one> ; push args to "set_numeric_error"
|
||
C_call set_nume ; set_numeric_error(1,ST_OFF_ERR,tmp_reg);
|
||
restore <SI> ; load offset of next instruction
|
||
jmp sch_err ; Link to Scheme debugger
|
||
ELSE
|
||
mov DX,4 ; STRING-SET! is 4 bytes long
|
||
jmp s_out_bn ; continue error processing
|
||
ENDIF
|
||
endm
|
||
|
||
;************************************************************************
|
||
;* AL AH *
|
||
;* (string-ref string index) string-ref str,index *
|
||
;************************************************************************
|
||
public st_ref
|
||
st_ref: st_thing REF,m_st_ref
|
||
|
||
;************************************************************************
|
||
;* AL AH AL *
|
||
;* (string-set! string index char) string-set! str,index,char *
|
||
;************************************************************************
|
||
public st_set
|
||
st_set: st_thing SET,m_st_set
|
||
|
||
purge st_thing
|
||
|
||
prog ends
|
||
end
|
||
|