hbmap: fix iterator truncation when size_t < 32bit
[rofl0r-agsutils.git] / Assembler.c
blobad814bdc8d28590f47bb7c54b704a7fc0b2280bf
1 #define _GNU_SOURCE
2 #include "endianness.h"
3 #include "File.h"
4 #include "ByteArray.h"
5 #include "MemGrow.h"
6 #include "Script_internal.h"
7 #include "List.h"
8 #include <ctype.h>
9 #include <stdio.h>
10 #include <assert.h>
11 #include <string.h>
12 #include <stdlib.h>
13 #include "Assembler.h"
14 #include "kw_search.h"
16 struct fixup {
17 int type;
18 unsigned offset;
21 struct string {
22 size_t len;
23 char* ptr;
26 struct label {
27 char* name;
28 unsigned insno;
31 struct sections_data {
32 char *name;
33 /* the offset points inside .text section to denote the start
34 of contents originating from a different file; to establish a mapping
35 between 'sourceline' statements and original file for debugging.
36 this happens when the editor converts and merges multiple dialog
37 script into a single script.
38 the number denotes the number of instructions to skip after start of
39 .text section, or using absolute offsets: (absolute_offset - offset_of_text)/4
40 example: when disassembled with -o, a dialogscript i tested looks
41 like this:
42 .text
43 ; offset: 24 (insno 0)
44 ...
45 ; offset: 27856 (insno 6958)
46 _run_dialog1$1: ; 1 args
47 sourceline 1
48 ; offset: 27864 (insno 6960)
49 ...
50 .sections
51 "__DialogScripts.asc" = 0
52 "Dialog 0" = 317
53 "Dialog 1" = 6958
54 ...
55 so _run_dialog1 starts at physical offset 27856, -24 = 27832,
56 27832/4= 6958.
57 in order to keep these offsets accurate after re-assembling (especially
58 after changes have been made, we should actually save the functionname
59 it points to, then after assembling put the so-calculated offset of
60 that same function there.
61 however since these sections are only interesting for debugging,
62 and only used (except those always at offset 0) in dialogscripts,
63 this is currently out of scope.
65 unsigned offset;
68 struct variable {
69 char* name;
70 unsigned vs;
71 unsigned offset;
74 static int add_label(AS *a, char* name, size_t insno) {
75 char* tmp = strdup(name);
76 return htab_insert(a->label_map, tmp, HTV_N(insno)) != 0;
79 static unsigned get_label_offset(AS *a, char* name) {
80 htab_value *ret = htab_find(a->label_map, name);
81 if(!ret) {
82 fprintf(stderr, "error: label '%s' not found\n", name);
83 if(strncmp(name, "label", 5)) fprintf(stderr, "hint: label names must start with 'label'\n");
84 exit(1);
86 return ret->n;
89 static int add_label_ref(AS *a, char * name, size_t insno) {
90 /* add reference to named label to a list. after the first pass
91 * over the code these locations have to be fixed with the offset
92 * of the label. */
93 struct label item = { .name = strdup(name), .insno = insno };
94 assert(item.name);
95 return List_add(a->label_ref_list, &item);
98 static int add_function_ref(AS *a, char* name, size_t insno) {
99 /* add reference to named function to a list. after the first pass
100 * over the code these locations have to be fixed with the offset
101 * of the label. */
102 struct label item = { .name = strdup(name), .insno = insno };
103 assert(item.name);
104 return List_add(a->function_ref_list, &item);
107 static int add_export(AS *a, int type, char* name, size_t offset) {
108 struct export item = { .fn = strdup(name), .instr = offset, .type = type};
109 assert(item.fn);
110 assert(List_add(a->export_list, &item));
111 assert(htab_insert(a->export_map, item.fn, HTV_N(List_size(a->export_list)-1)));
112 return 1;
115 static int add_fixup(AS *a, int type, size_t offset) {
116 struct fixup item = {.type = type, .offset = offset};
117 /* offset equals instruction number for non-DATADATA fixups */
118 return List_add(a->fixup_list, &item);
121 static int add_sections_name(AS *a, char* name, int value) {
122 struct sections_data item = {.name = strdup(name), .offset = value};
123 return List_add(a->sections_list, &item);
126 static size_t add_or_get_string__offset(AS* a, char* str) {
127 /* return offset of string in string table
128 * add to string table if not yet existing */
129 str++; /* leading '"' */
130 size_t l = strlen(str), o;
131 l--;
132 str[l] = 0; /* trailing '"' */
134 htab_value *v = htab_find(a->string_offset_map, str);
135 if(v) return v->n;
137 struct string item = {.ptr = strdup(str), .len = l };
138 o = a->string_section_length;
139 assert(List_add(a->string_list, &item));
140 assert(htab_insert(a->string_offset_map, item.ptr, HTV_N(o)));
141 a->string_section_length += l + 1;
142 return o;
145 static size_t get_string_section_length(AS* a) {
146 return a->string_section_length;
149 static int add_variable(AS *a, char* name, unsigned vs, size_t offset) {
150 struct variable item = { .name = strdup(name), .vs = vs, .offset = offset };
151 return List_add(a->variable_list, &item);
154 static int get_variable_offset(AS* a, char* name) {
155 /* return globaldata offset of named variable */
156 size_t i = 0;
157 struct variable *item;
158 for(; i < List_size(a->variable_list); i++) {
159 assert((item = List_getptr(a->variable_list, i)));
160 if(!strcmp(item->name, name))
161 return item->offset;
163 fprintf(stderr, "error: variable '%s' not found\n", name);
164 assert(0);
165 return 0;
168 static ssize_t find_section(FILE* in, char* name, size_t *lineno) {
169 char buf[1024];
170 size_t off = 0, l = strlen(name);
171 *lineno = 0;
172 fseek(in, 0, SEEK_SET);
173 while(fgets(buf, sizeof buf, in)) {
174 *lineno = *lineno +1;
175 off += strlen(buf);
176 if(buf[0] == '.' && memcmp(name, buf + 1, l) == 0)
177 return off;
179 return -1;
182 static int asm_data(AS* a) {
183 size_t lineno;
184 ssize_t start = find_section(a->in, "data", &lineno);
185 if(start == -1) return 1; // it is valid for .s file to only have .text
186 fseek(a->in, start, SEEK_SET);
187 char buf[1024];
188 size_t data_pos = 0;
189 while(fgets(buf, sizeof buf, a->in) && buf[0] != '.') {
190 if(buf[0] == '\n') continue;
191 char* p = buf, *pend = buf + sizeof buf, *var;
192 int exportflag = 0;
193 unsigned vs = 0;
194 if(*p == '#' || *p == ';') continue;
195 while(isspace(*p) && p < pend) p++;
196 if(*p == ';') continue;
197 if(!memcmp(p, "export", 6) && isspace(p[6])) {
198 p += 7;
199 exportflag = 1;
200 while(isspace(*p) && p < pend) p++;
202 if(memcmp(p, "int", 3) == 0)
203 vs = 4;
204 else if(memcmp(p, "short", 5) == 0)
205 vs = 2;
206 else if(memcmp(p, "char", 4) == 0) {
207 vs = 1;
208 if(p[4] == '[') {
209 vs = atoi(p+5);
210 char *q = p+5;
211 while(isdigit(*q) && q < pend) q++;
212 if(vs == 0 || *q != ']') {
213 fprintf(stderr, "error: expected number > 0 and ']' after '['\n");
214 return 0;
217 else vs = 1;
218 } else if(memcmp(p, "string", 6) == 0)
219 vs = 200;
220 else {
221 fprintf(stderr, "error: expected int, short, char, or string\n");
222 return 0;
224 while(!isspace(*p) && p < pend) p++;
225 while(isspace(*p) && p < pend) p++;
226 var = p;
227 while(!isspace(*p) && p < pend) p++;
228 *p = 0; p++;
229 assert(p < pend && *p == '=');
230 p++; while(isspace(*p) && p < pend) p++;
231 assert(p < pend);
232 int value;
234 if(*p == '.') {
235 p++;
236 if(memcmp(p, "data", 4) == 0) {
237 p += 4;
238 while(isspace(*p) && p < pend) p++;
239 assert(p < pend && *p == '+');
240 p++;
241 while(isspace(*p) && p < pend) p++;
242 value = atoi(p);
243 add_fixup(a, FIXUP_DATADATA, data_pos);
244 goto write_var;
245 } else {
246 fprintf(stderr, "error: expected \"data\"\n");
247 return 0;
249 } else {
250 value = atoi(p);
251 write_var:
252 switch (vs) {
253 default:
254 for(value = vs; value >= 10; value-=10)
255 ByteArray_writeMem(a->data, (void*)"\0\0\0\0\0\0\0\0\0\0", 10);
256 while(value--) ByteArray_writeUnsignedByte(a->data, 0);
257 break;
258 case 4:
259 ByteArray_writeInt(a->data, value);
260 break;
261 case 2:
262 ByteArray_writeShort(a->data, value);
263 break;
264 case 1:
265 ByteArray_writeUnsignedByte(a->data, value);
266 break;
269 if(exportflag) add_export(a, EXPORT_DATA, var, data_pos);
270 add_variable(a, var, vs, data_pos);
271 data_pos += vs;
273 return 1;
276 ssize_t get_import_index(AS* a, char* name, size_t len) {
277 (void) len;
278 htab_value *v = htab_find(a->import_map, name);
279 if(!v) return -1;
280 return v->n;
283 void add_import(AS *a, char* name) {
284 size_t l = strlen(name);
285 if(get_import_index(a, name, l) != -1) return;
286 struct string item;
287 item.ptr = strdup(name);
288 item.len = l;
289 assert(List_add(a->import_list, &item));
290 assert(htab_insert(a->import_map, item.ptr, HTV_N(List_size(a->import_list)-1)));
293 static int find_export(AS *a, int type, char* name, unsigned *offset) {
294 struct export *item;
295 htab_value *v = htab_find(a->export_map, name);
296 if(!v) return 0;
297 assert((item = List_getptr(a->export_list, v->n)));
298 assert(item->type == type && !strcmp(name, item->fn));
299 *offset = item->instr;
300 return 1;
303 void generate_import_table(AS *a) {
304 size_t i;
305 struct label *item;
306 unsigned off;
307 for(i = 0; i < List_size(a->function_ref_list); i++) {
308 assert((item = List_getptr(a->function_ref_list, i)));
309 if(!find_export(a, EXPORT_FUNCTION, item->name, &off))
310 add_import(a, item->name);
314 static int get_reg(char* regname) {
315 return kw_find_reg(regname, strlen(regname));
318 #include "StringEscape.h"
319 /* expects a pointer to the first char after a opening " in a string,
320 * converts the string into convbuf, and returns the length of that string */
321 static size_t get_length_and_convert(char* x, char* end, char* convbuf, size_t convbuflen) {
322 size_t result = 0;
323 char* e = x + strlen(x);
324 assert(e > x && e < end && *e == 0);
325 e--;
326 while(isspace(*e)) e--;
327 if(*e != '"') return (size_t) -1;
328 *e = 0;
329 result = unescape(x, convbuf, convbuflen);
330 return result;
333 /* sets lets char in arg to 0, and advances pointer till the next argstart */
334 static char* finalize_arg(char **p, char* pend, char* convbuf, size_t convbuflen) {
335 if(**p == '"') {
336 convbuf[0] = '"';
337 size_t l= get_length_and_convert(*p + 1, pend, convbuf+1, convbuflen - 1);
338 if(l == (size_t) -1) return 0;
339 convbuf[l+1] = '"';
340 convbuf[l+2] = 0;
341 *p = 0; /* make it crash if its accessed again, since a string should always be the last arg */
342 return convbuf;
343 } else {
344 char* ret = *p;
345 while(*p < pend && **p != ',' && !isspace(**p)) (*p)++;
346 assert(*p < pend);
347 **p = 0; (*p)++;
348 while(*p < pend && isspace(**p)) (*p)++;
349 assert(*p < pend);
350 return ret;
354 static int asm_strings(AS *a) {
355 /* add strings in .strings section, even when they're not used from .text */
356 size_t lineno;
357 ssize_t start = find_section(a->in, "strings", &lineno);
358 if(start == -1) return 1;
359 fseek(a->in, start, SEEK_SET);
360 char buf[1024];
361 while(fgets(buf, sizeof buf, a->in) && buf[0] != '.') {
362 char* p = buf;
363 if(*p == '#' || *p == ';') continue;
364 assert(*p == '"');
365 size_t l = strlen(p);
366 assert(l>1 && p[l-1] == '\n' && p[l-2] == '"');
367 p[l-1] = 0;
368 add_or_get_string__offset(a, p);
370 return 1;
373 static int asm_sections(AS *a) {
374 /* add sections in .sections section */
375 size_t lineno;
376 ssize_t start = find_section(a->in, "sections", &lineno);
377 if(start == -1) return 1;
378 fseek(a->in, start, SEEK_SET);
379 char buf[1024];
380 while(fgets(buf, sizeof buf, a->in) && buf[0] != '.') {
381 char* p = buf;
382 if(strchr("#;\n\r", *p)) continue;
383 assert(*p == '"');
384 size_t l = strlen(p);
385 assert(l>1 && p[l-1] == '\n');
386 char *e = strrchr(p, '=');
387 assert(e);
388 char *f = e;
389 while(--f > p && isspace(*f));
390 assert(f > p && *f == '"');
391 *f = 0;
392 while(isspace(*(++e)));
393 int val = atoi(e);
394 add_sections_name(a, p+1, val);
396 return 1;
399 static int asm_text(AS *a) {
400 size_t lineno;
401 ssize_t start = find_section(a->in, "text", &lineno);
402 if(start == -1) return 1;
403 fseek(a->in, start, SEEK_SET);
404 char buf[1024];
405 char convbuf[sizeof(buf)]; /* to convert escaped string into non-escaped version */
406 size_t pos = 0;
407 while(fgets(buf, sizeof buf, a->in) && buf[0] != '.') {
408 lineno++;
409 char* p = buf, *pend = buf + sizeof buf;
410 if(*p == '#' || *p == ';') continue;
411 while(isspace(*p) && p < pend) p++;
412 assert(p < pend);
413 if(!*p || *p == ';') continue;
414 char* sym = p;
415 while(!isspace(*p) && p < pend) p++;
416 *p = 0; p++;
417 size_t l = strlen(sym);
418 if(l > 1 && sym[l-1] == ':') {
419 // functionstart or label
420 sym[l-1] = 0;
421 if(memcmp(sym, "label", 5) == 0)
422 add_label(a, sym, pos);
423 else {
424 add_export(a, EXPORT_FUNCTION, sym, pos);
425 ByteArray_writeUnsignedInt(a->code, SCMD_THISBASE);
426 ByteArray_writeUnsignedInt(a->code, pos);
427 pos+=2;
429 continue;
431 unsigned instr = kw_find_insn(sym, l);
432 if(!instr) {
433 fprintf(stderr, "line %zu: error: unknown instruction '%s'\n", lineno, sym);
434 return 0;
436 if(instr == SCMD_THISBASE) continue; /* we emit this instruction ourselves when a new function starts. */
438 ByteArray_writeUnsignedInt(a->code, instr);
439 pos++;
440 size_t arg;
441 for(arg = 0; arg < opcodes[instr].argcount; arg++) {
442 sym = finalize_arg(&p, pend, convbuf, sizeof(convbuf));
443 if(sym == 0) {
444 fprintf(stderr, "line %zu: error: expected \"\n", lineno);
445 return 0;
447 int value = 0;
448 if(arg < opcodes[instr].regcount) {
449 value=get_reg(sym);
450 if(instr == SCMD_REGTOREG) {
451 /* fix reversed order of arguments */
452 int dst = value;
453 sym = p;
454 while(p < pend && *p != ',' && !isspace(*p)) p++;
455 assert(p < pend);
456 *p = 0;
457 value=get_reg(sym);
458 ByteArray_writeInt(a->code, value);
459 ByteArray_writeInt(a->code, dst);
460 pos += 2;
461 break;
463 } else {
464 switch(instr) {
465 case SCMD_LITTOREG:
466 /* immediate can be function name, string,
467 * variable name, stack fixup, or numeric value */
468 if(sym[0] == '"') {
469 value = add_or_get_string__offset(a, sym);
470 add_fixup(a, FIXUP_STRING, pos);
471 } else if(sym[0] == '@') {
472 value = get_variable_offset(a, sym+1);
473 add_fixup(a, FIXUP_GLOBALDATA, pos);
474 } else if(sym[0] == '.') {
475 if(memcmp(sym+1, "stack", 5)) {
476 fprintf(stderr, "error: expected stack\n");
477 return 0;
479 sym += 6;
480 while(isspace(*sym) && sym < pend) sym++;
481 assert(sym < pend && *sym == '+');
482 sym++;
483 while(isspace(*sym) && sym < pend) sym++;
484 add_fixup(a, FIXUP_STACK, pos);
485 value = atoi(sym);
486 } else if(isdigit(sym[0]) || sym[0] == '-') {
487 if(sym[0] == '-') assert(isdigit(sym[1]));
488 value = atoi(sym);
489 } else
490 add_function_ref(a, sym, pos);
491 break;
492 case SCMD_JMP: case SCMD_JZ: case SCMD_JNZ:
493 add_label_ref(a, sym, pos);
494 break;
495 default:
496 value = atoi(sym);
499 ByteArray_writeInt(a->code, value);
500 pos++;
504 size_t i;
505 struct label *item;
506 for(i = 0; i < List_size(a->label_ref_list); i++) {
507 assert((item = List_getptr(a->label_ref_list, i)));
508 ByteArray_set_position(a->code, item->insno * 4);
509 int lbl = get_label_offset(a, item->name);
510 assert(lbl >= 0 && lbl < pos);
511 int label_insno = lbl - (item->insno+1); /* offset is calculated from next instruction */
512 ByteArray_writeInt(a->code, label_insno);
514 generate_import_table(a);
515 for(i = 0; i < List_size(a->function_ref_list); i++) {
516 assert((item = List_getptr(a->function_ref_list, i)));
517 ssize_t imp = get_import_index(a, item->name, strlen(item->name));
518 if(imp == -1) {
519 unsigned off;
520 assert(find_export(a, EXPORT_FUNCTION, item->name, &off));
521 imp = off;
522 add_fixup(a, FIXUP_FUNCTION, item->insno);
523 } else {
524 add_fixup(a, FIXUP_IMPORT, item->insno);
526 assert(imp != -1);
527 ByteArray_set_position(a->code, item->insno * 4);
528 ByteArray_writeInt(a->code, imp);
531 return 1;
534 static void write_int(FILE* o, int val) {
535 val = end_htole32(val);
536 fwrite(&val, 4, 1, o);
539 static int fixup_comparefunc(const void *a, const void* b) {
540 const struct fixup* fa = a, *fb = b;
541 if(fa->type == FIXUP_DATADATA && fb->type != FIXUP_DATADATA)
542 return -1;
543 if(fb->type == FIXUP_DATADATA && fa->type != FIXUP_DATADATA)
544 return 1;
545 if(fa->offset < fb->offset) return -1;
546 if(fa->offset == fb->offset) return 0;
547 return 1;
550 static void sort_fixup_list(AS* a) {
551 List_sort(a->fixup_list, fixup_comparefunc);
554 static void write_fixup_list(AS* a, FILE *o) {
555 struct fixup *item;
556 size_t i;
557 for(i = 0; i < List_size(a->fixup_list); i++) {
558 assert((item = List_getptr(a->fixup_list, i)));
559 char type = item->type;
560 fwrite(&type, 1, 1, o);
562 for(i = 0; i < List_size(a->fixup_list); i++) {
563 assert((item = List_getptr(a->fixup_list, i)));
564 write_int(o, item->offset);
568 static void write_string_section(AS* a, FILE* o) {
569 struct string item;
570 size_t i = 0;
571 for(; i < List_size(a->string_list); i++) {
572 assert(List_get(a->string_list, i, &item));
573 fwrite(item.ptr, item.len + 1, 1, o);
577 static void write_import_section(AS* a, FILE* o) {
578 struct string item;
579 size_t i = 0;
580 for(; i < List_size(a->import_list); i++) {
581 assert(List_get(a->import_list, i, &item));
582 fwrite(item.ptr, item.len + 1, 1, o);
586 static void write_export_section(AS* a, FILE* o) {
587 struct export item;
588 size_t i = 0;
589 for(; i < List_size(a->export_list); i++) {
590 assert(List_get(a->export_list, i, &item));
591 fwrite(item.fn, strlen(item.fn) + 1, 1, o);
592 unsigned encoded = (item.type << 24) | (item.instr &0x00FFFFFF);
593 write_int(o, encoded);
597 static void write_sections_section(AS* a, FILE *o) {
598 struct sections_data item;
599 size_t i = 0;
600 for(; i < List_size(a->sections_list); i++) {
601 assert(List_get(a->sections_list, i, &item));
602 fwrite(item.name, strlen(item.name) + 1, 1, o);
603 write_int(o, item.offset);
604 break; // FIXME : currently writing only first item - dialogscripts have more than one
608 static int write_object(AS *a, char *out) {
609 FILE *o;
610 if(!(o = fopen(out, "wb"))) return 0;
611 fprintf(o, "SCOM");
612 write_int(o, 83); //version
613 write_int(o, ByteArray_get_length(a->data)); // globaldatasize
614 write_int(o, ByteArray_get_length(a->code) / 4); // codesize
615 write_int(o, get_string_section_length(a)); // stringssize
616 size_t l = ByteArray_get_length(a->data);
617 void *p;
618 if(l) {
619 p = ByteArray_get_mem(a->data, 0, l);
620 assert(p);
621 fwrite(p,l,1,o); // globaldata
623 l = ByteArray_get_length(a->code);
624 if(l) {
625 p = ByteArray_get_mem(a->code, 0, l);
626 assert(p);
627 fwrite(p,l,1,o); // code
629 write_string_section(a, o);
630 write_int(o, List_size(a->fixup_list));
631 sort_fixup_list(a);
632 write_fixup_list(a, o);
633 if(!List_size(a->import_list)) {
634 /* AGS declares object files with 0 imports as invalid */
635 add_import(a, "");
637 write_int(o, List_size(a->import_list));
638 write_import_section(a, o);
639 write_int(o, List_size(a->export_list));
640 write_export_section(a, o);
641 write_int(o, List_size(a->sections_list) ? 1 : 0); // FIXME we currently on write first section
642 write_sections_section(a, o);
643 write_int(o, 0xbeefcafe); // magic end marker.
644 fclose(o);
645 return 1;
648 int AS_assemble(AS* a, char* out) {
649 if(!asm_data(a)) return 0;
650 if(!asm_text(a)) return 0;
651 // if(!asm_strings(a)) return 0; // emitting unneeded strings is not necessary
652 if(!asm_sections(a)) return 0;
653 if(!write_object(a, out)) return 0;
654 return 1;
657 void AS_open_stream(AS* a, FILE* f) {
658 memset(a, 0, sizeof *a);
659 a->obj = &a->obj_b;
660 a->data = &a->data_b;
661 a->code = &a->code_b;
662 ByteArray_ctor(a->obj);
663 ByteArray_open_mem(a->obj, 0, 0);
664 ByteArray_ctor(a->data);
665 ByteArray_set_endian(a->data, BAE_LITTLE);
666 ByteArray_set_flags(a->data, BAF_CANGROW);
667 ByteArray_open_mem(a->data, 0, 0);
668 ByteArray_ctor(a->code);
669 ByteArray_set_endian(a->code, BAE_LITTLE);
670 ByteArray_set_flags(a->code, BAF_CANGROW);
671 ByteArray_open_mem(a->code, 0, 0);
673 a->export_list = &a->export_list_b;
674 a->fixup_list = &a->fixup_list_b;
675 a->string_list = &a->string_list_b;
676 a->label_ref_list = &a->label_ref_list_b;
677 a->function_ref_list = &a->function_ref_list_b;
678 a->variable_list = &a->variable_list_b;
679 a->import_list = &a->import_list_b;
680 a->sections_list = &a->sections_list_b;
682 a->label_map = htab_create(128);
683 a->import_map = htab_create(128);
684 a->export_map = htab_create(128);
685 a->string_offset_map = htab_create(128);
687 List_init(a->export_list, sizeof(struct export));
688 List_init(a->fixup_list , sizeof(struct fixup));
689 List_init(a->string_list, sizeof(struct string));
690 List_init(a->label_ref_list, sizeof(struct label));
691 List_init(a->function_ref_list, sizeof(struct label));
692 List_init(a->variable_list, sizeof(struct variable));
693 List_init(a->import_list, sizeof(struct string));
694 List_init(a->sections_list, sizeof(struct sections_data));
696 a->in = f;
697 a->string_section_length = 0;
698 kw_init();
701 int AS_open(AS* a, char* fn) {
702 FILE *f = fopen(fn, "rb");
703 if(!f) return 0;
704 AS_open_stream(a, f);
705 return 1;
709 void AS_close(AS* a) {
710 fclose(a->in);
711 kw_finish();