inweb-bootstrap/Chapter_3/The_Analyser.nw

435 lines
17 KiB
Text
Raw Permalink Normal View History

2019-02-04 22:26:45 +00:00
[Analyser::] The Analyser.
Here we analyse the code in the web, enabling us to see how functions
and data structures are used within the program.
2024-03-09 06:17:52 +00:00
@ \section{Scanning webs.}
2019-02-04 22:26:45 +00:00
This scanner is intended for debugging Inweb, and simply shows the main
result of reading in and parsing the web:
2024-03-09 06:17:52 +00:00
<<*>>=
2019-02-04 22:26:45 +00:00
void Analyser::scan_line_categories(web *W, text_stream *range) {
PRINT("Scan of source lines for '%S'\n", range);
int count = 1;
chapter *C = Reader::get_chapter_for_range(W, range);
if (C) {
section *S;
LOOP_OVER_LINKED_LIST(S, section, C->sections)
for (source_line *L = S->first_line; L; L = L->next_line)
2024-03-09 06:17:52 +00:00
<<Trace the content and category of this source line>>;
2019-02-04 22:26:45 +00:00
} else {
section *S = Reader::get_section_for_range(W, range);
if (S) {
for (source_line *L = S->first_line; L; L = L->next_line)
2024-03-09 06:17:52 +00:00
<<Trace the content and category of this source line>>
2019-02-04 22:26:45 +00:00
} else {
LOOP_OVER_LINKED_LIST(C, chapter, W->chapters)
LOOP_OVER_LINKED_LIST(S, section, C->sections)
for (source_line *L = S->first_line; L; L = L->next_line)
2024-03-09 06:17:52 +00:00
<<Trace the content and category of this source line>>;
2019-02-04 22:26:45 +00:00
}
}
}
2024-03-09 06:17:52 +00:00
<<Trace the content and category of this source line>>=
2020-06-27 22:03:14 +00:00
TEMPORARY_TEXT(C)
2019-02-04 22:26:45 +00:00
WRITE_TO(C, "%s", Lines::category_name(L->category));
while (Str::len(C) < 20) PUT_TO(C, '.');
PRINT("%07d %S %S\n", count++, C, L->text);
2020-06-27 22:03:14 +00:00
DISCARD_TEXT(C)
2019-02-04 22:26:45 +00:00
2024-03-09 06:17:52 +00:00
@ \section{The section catalogue.}
2019-02-04 22:26:45 +00:00
This provides quite a useful overview of the sections. As we'll see frequently
in Chapter 4, we call out to a general routine in Chapter 5 to provide
annotations which are programming-language specific; the aim is to abstract
so that Chapter 4 contains no assumptions about the language.
2024-03-09 06:17:52 +00:00
<<*>>=
enum BASIC_SECTIONCAT from 1
enum STRUCTURES_SECTIONCAT
enum FUNCTIONS_SECTIONCAT
2019-02-04 22:26:45 +00:00
2024-03-09 06:17:52 +00:00
<<*>>=
2019-02-04 22:26:45 +00:00
void Analyser::catalogue_the_sections(web *W, text_stream *range, int form) {
int max_width = 0, max_range_width = 0;
chapter *C;
section *S;
LOOP_OVER_LINKED_LIST(C, chapter, W->chapters)
LOOP_OVER_LINKED_LIST(S, section, C->sections) {
2020-04-12 16:24:23 +00:00
if (max_range_width < Str::len(S->md->sect_range)) max_range_width = Str::len(S->md->sect_range);
2020-06-27 22:03:14 +00:00
TEMPORARY_TEXT(main_title)
2020-04-10 08:11:09 +00:00
WRITE_TO(main_title, "%S/%S", C->md->ch_basic_title, S->md->sect_title);
2019-02-04 22:26:45 +00:00
if (max_width < Str::len(main_title)) max_width = Str::len(main_title);
2020-06-27 22:03:14 +00:00
DISCARD_TEXT(main_title)
2019-02-04 22:26:45 +00:00
}
LOOP_OVER_LINKED_LIST(C, chapter, W->chapters)
if ((Str::eq_wide_string(range, L"0")) || (Str::eq(range, C->md->ch_range))) {
2019-02-04 22:26:45 +00:00
PRINT(" -----\n");
LOOP_OVER_LINKED_LIST(S, section, C->sections) {
2020-06-27 22:03:14 +00:00
TEMPORARY_TEXT(main_title)
2020-04-10 08:11:09 +00:00
WRITE_TO(main_title, "%S/%S", C->md->ch_basic_title, S->md->sect_title);
2020-04-12 16:24:23 +00:00
PRINT("%4d %S", S->sect_extent, S->md->sect_range);
for (int i = Str::len(S->md->sect_range); i<max_range_width+2; i++) PRINT(" ");
2019-02-04 22:26:45 +00:00
PRINT("%S", main_title);
for (int i = Str::len(main_title); i<max_width+2; i++) PRINT(" ");
if (form != BASIC_SECTIONCAT)
Functions::catalogue(S, (form == FUNCTIONS_SECTIONCAT)?TRUE:FALSE);
2019-02-04 22:26:45 +00:00
PRINT("\n");
2020-06-27 22:03:14 +00:00
DISCARD_TEXT(main_title)
2019-02-04 22:26:45 +00:00
}
}
}
2024-03-09 06:17:52 +00:00
@ \section{Analysing code.}
2019-02-04 22:26:45 +00:00
We can't pretend to a full-scale static analysis of the code -- for one thing,
that would mean knowing more about the syntax of the web's language than we
actually do. So the following provides only a toolkit which other code can
use when looking for certain syntactic patterns: something which looks like
a function call, or a C structure field reference, for example. These are
all essentially based on spotting identifiers in the code, but with
punctuation around them.
Usage codes are used to define a set of allowed contexts in which to spot
these identifiers.
2024-03-09 06:17:52 +00:00
<<*>>
#define ELEMENT_ACCESS_USAGE 0x00000001 /* C-like languages: access via [[->| or |.]] operators to structure element */
#define FCALL_USAGE 0x00000002 /* C-like languages: function call made using brackets, [[name(args)]] */
#define PREFORM_IN_CODE_USAGE 0x00000004 /* InC only: use of a Preform nonterminal as a C "constant" */
#define PREFORM_IN_GRAMMAR_USAGE 0x00000008 /* InC only: ditto, but within Preform production rather than C code */
#define MISC_USAGE 0x00000010 /* any other appearance as an identifier */
#define ANY_USAGE 0x7fffffff /* any of the above */
2019-02-04 22:26:45 +00:00
@ The main analysis routine goes through a web as follows. Note that we only
perform the search here, we don't comment on the results; any action to be
2024-03-09 06:17:52 +00:00
taken must be handled by [[LanguageMethods::late_preweave_analysis]] when we're done.
2019-02-04 22:26:45 +00:00
2024-03-09 06:17:52 +00:00
<<*>>=
2019-02-04 22:26:45 +00:00
void Analyser::analyse_code(web *W) {
if (W->analysed) return;
2024-03-09 06:17:52 +00:00
<<Ask language-specific code to identify search targets, and parse the Interfaces>>;
2019-02-04 22:26:45 +00:00
chapter *C;
section *S;
LOOP_WITHIN_TANGLE(C, S, Tangler::primary_target(W))
switch (L->category) {
case BEGIN_DEFINITION_LCAT:
2024-03-09 06:17:52 +00:00
<<Perform analysis on the body of the definition>>;
2019-02-04 22:26:45 +00:00
break;
case CODE_BODY_LCAT:
2024-03-09 06:17:52 +00:00
<<Perform analysis on a typical line of code>>;
2019-02-04 22:26:45 +00:00
break;
case PREFORM_GRAMMAR_LCAT:
2024-03-09 06:17:52 +00:00
<<Perform analysis on productions in a Preform grammar>>;
2019-02-04 22:26:45 +00:00
break;
}
2020-04-04 19:46:43 +00:00
LanguageMethods::late_preweave_analysis(W->main_language, W);
2019-02-04 22:26:45 +00:00
W->analysed = TRUE;
}
@ First, we call any language-specific code, whose task is to identify what we
should be looking for: for example, the C-like languages code tells us (see
below) to look for names of particular functions it knows about.
In Version 1 webs, this code is also expected to parse any Interface lines in
a section which it recognises, marking those by setting their
2024-03-09 06:17:52 +00:00
[[interface_line_identified]] flags. Any that are left must be erroneous.
2019-02-04 22:26:45 +00:00
Version 2 removed Interface altogeter as being cumbersome for no real gain in
practice.
2024-03-09 06:17:52 +00:00
<<Ask language-specific code to identify search targets, and parse the Interfaces>>=
2020-04-04 19:46:43 +00:00
LanguageMethods::early_preweave_analysis(W->main_language, W);
2019-02-04 22:26:45 +00:00
chapter *C;
section *S;
LOOP_WITHIN_TANGLE(C, S, Tangler::primary_target(W))
if ((L->category == INTERFACE_BODY_LCAT) &&
(L->interface_line_identified == FALSE) &&
(Regexp::string_is_white_space(L->text) == FALSE))
Main::error_in_web(I"unrecognised interface line", L);
2024-03-09 06:17:52 +00:00
<<Perform analysis on a typical line of code>>=
2019-02-04 22:26:45 +00:00
Analyser::analyse_as_code(W, L, L->text, ANY_USAGE, 0);
2024-03-09 06:17:52 +00:00
<<Perform analysis on the body of the definition>>=
2019-02-04 22:26:45 +00:00
Analyser::analyse_as_code(W, L, L->text_operand2, ANY_USAGE, 0);
while ((L->next_line) && (L->next_line->category == CONT_DEFINITION_LCAT)) {
L = L->next_line;
Analyser::analyse_as_code(W, L, L->text, ANY_USAGE, 0);
}
2020-04-11 20:39:43 +00:00
@ Lines in a Preform grammar generally take the form of some BNF grammar, where
2024-03-09 06:17:52 +00:00
we want only to identify any nonterminals mentioned, then a [[==>]] divider,
2019-02-04 22:26:45 +00:00
and then some C code to deal with a match. The code is subjected to analysis
just as any other code would be.
2024-03-09 06:17:52 +00:00
<<Perform analysis on productions in a Preform grammar>>=
2020-05-07 18:11:08 +00:00
Analyser::analyse_as_code(W, L, L->text_operand2,
ANY_USAGE, 0);
Analyser::analyse_as_code(W, L, L->text_operand,
PREFORM_IN_CODE_USAGE, PREFORM_IN_GRAMMAR_USAGE);
2019-02-04 22:26:45 +00:00
2024-03-09 06:17:52 +00:00
@ \section{Identifier searching.}
Here's what we actually do, then. We take the code fragment [[text]], drawn
from part or all of source line [[L]] from web [[W]], and look for any identifier
names used in one of the contexts in the bitmap [[mask]]. Any that we find are
passed to [[Analyser::analyse_find]], along with the context they were found in (or, if
[[transf]] is nonzero, with [[transf]] as their context).
2019-02-04 22:26:45 +00:00
What we do is to look for instances of an identifier, defined as a maximal
2024-03-09 06:17:52 +00:00
string of [[%i]] characters or hyphens not followed by [[>]] characters. (Thus
[[fish-or-chips]] counts, but [[fish-]] is not an identifier when it occurs in
[[fish->bone]].)
2019-02-04 22:26:45 +00:00
2024-03-09 06:17:52 +00:00
<<*>>=
2019-02-04 22:26:45 +00:00
void Analyser::analyse_as_code(web *W, source_line *L, text_stream *text, int mask, int transf) {
int start_at = -1, element_follows = FALSE;
for (int i = 0; i < Str::len(text); i++) {
if ((Regexp::identifier_char(Str::get_at(text, i))) ||
((Str::get_at(text, i) == '-') && (Str::get_at(text, i+1) != '>'))) {
if (start_at == -1) start_at = i;
} else {
2024-03-09 06:17:52 +00:00
if (start_at != -1) <<Found an identifier>>;
2019-02-04 22:26:45 +00:00
if (Str::get_at(text, i) == '.') element_follows = TRUE;
else if ((Str::get_at(text, i) == '-') && (Str::get_at(text, i+1) == '>')) {
element_follows = TRUE; i++;
} else element_follows = FALSE;
}
}
2020-04-28 22:17:48 +00:00
if (start_at != -1) {
int i = Str::len(text);
2024-03-09 06:17:52 +00:00
<<Found an identifier>>;
2020-04-28 22:17:48 +00:00
}
2019-02-04 22:26:45 +00:00
}
2024-03-09 06:17:52 +00:00
<<Found an identifier>>=
2020-04-28 22:17:48 +00:00
int u = MISC_USAGE;
if (element_follows) u = ELEMENT_ACCESS_USAGE;
else if (Str::get_at(text, i) == '(') u = FCALL_USAGE;
else if ((Str::get_at(text, i) == '>') && (start_at > 0) && (Str::get_at(text, start_at-1) == '<'))
u = PREFORM_IN_CODE_USAGE;
if (u & mask) {
if (transf) u = transf;
2020-06-27 22:03:14 +00:00
TEMPORARY_TEXT(identifier_found)
2020-04-28 22:17:48 +00:00
for (int j = 0; start_at + j < i; j++)
PUT_TO(identifier_found, Str::get_at(text, start_at + j));
Analyser::analyse_find(W, L, identifier_found, u);
2020-06-27 22:03:14 +00:00
DISCARD_TEXT(identifier_found)
2020-04-28 22:17:48 +00:00
}
start_at = -1; element_follows = FALSE;
2024-03-09 06:17:52 +00:00
@ \section{The identifier hash table.}
2019-02-04 22:26:45 +00:00
We clearly need rapid access to a large symbols table, and we store this as
a hash. Identifiers are hash-coded with the following simple code, which is
simplified from one used by Inform; it's the algorithm called "X 30011"
in Aho, Sethi and Ullman, "Compilers: Principles, Techniques and Tools"
(1986), adapted slightly to separate out literal numbers.
2024-03-09 06:17:52 +00:00
<<*>>=
#define HASH_TAB_SIZE 1000 /* the possible hash codes are 0 up to this minus 1 */
#define NUMBER_HASH 0 /* literal decimal integers, and no other words, have this hash code */
2019-02-04 22:26:45 +00:00
2024-03-09 06:17:52 +00:00
<<*>>=
2019-02-04 22:26:45 +00:00
int Analyser::hash_code_from_word(text_stream *text) {
unsigned int hash_code = 0;
string_position p = Str::start(text);
switch(Str::get(p)) {
case '-': if (Str::len(text) == 1) break; /* an isolated minus sign is an ordinary word */
/* and otherwise fall through to... */
2019-02-04 22:26:45 +00:00
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9': {
int numeric = TRUE;
2019-02-04 22:26:45 +00:00
/* the first character may prove to be the start of a number: is this true? */
for (p = Str::forward(p); Str::in_range(p); p = Str::forward(p))
if (isdigit(Str::get(p)) == FALSE) numeric = FALSE;
if (numeric) return NUMBER_HASH;
}
2019-02-04 22:26:45 +00:00
}
for (p=Str::start(text); Str::in_range(p); p = Str::forward(p))
hash_code = (unsigned int) ((int) (hash_code*30011) + (Str::get(p)));
return (int) (1+(hash_code % (HASH_TAB_SIZE-1))); /* result of X 30011, plus 1 */
}
@ The actual table is stored here:
2024-03-09 06:17:52 +00:00
<<*>>=
#define HASH_SAFETY_CODE 0x31415927
2024-03-09 06:17:52 +00:00
<<*>>=
2019-02-04 22:26:45 +00:00
typedef struct hash_table {
2024-03-09 06:17:52 +00:00
struct linked_list *analysis_hash[HASH_TAB_SIZE]; /* of [[hash_table_entry]] */
int safety_code; /* when we start up, array's contents are undefined, so... */
2019-02-04 22:26:45 +00:00
} hash_table;
void Analyser::initialise_hash_table(hash_table *HT) {
HT->safety_code = HASH_SAFETY_CODE;
for (int i=0; i<HASH_TAB_SIZE; i++) HT->analysis_hash[i] = NULL;
}
2019-02-04 22:26:45 +00:00
@ Where we define:
2024-03-09 06:17:52 +00:00
<<*>>=
2019-02-04 22:26:45 +00:00
typedef struct hash_table_entry {
text_stream *hash_key;
int language_reserved_word; /* in the language currently being woven, that is */
2024-03-09 06:17:52 +00:00
struct linked_list *usages; /* of [[hash_table_entry_usage]] */
2020-04-11 20:39:43 +00:00
struct source_line *definition_line; /* or null, if it's not a constant, function or type name */
struct language_function *as_function; /* for function names only */
2020-05-09 12:05:00 +00:00
CLASS_DEFINITION
2019-02-04 22:26:45 +00:00
} hash_table_entry;
@ A single routine is used both to interrogate the hash and to lodge values
in it, as usual with symbols tables. For example, the code to handle C-like
languages prepares for code analysis by calling this routine on the name
of each C function.
2024-03-09 06:17:52 +00:00
<<*>>=
2020-04-05 17:37:43 +00:00
hash_table_entry *Analyser::find_hash_entry(hash_table *HT, text_stream *text, int create) {
2019-02-04 22:26:45 +00:00
int h = Analyser::hash_code_from_word(text);
if (h == NUMBER_HASH) return NULL;
if ((h<0) || (h>=HASH_TAB_SIZE)) internal_error("hash code out of range");
if (HT->safety_code != HASH_SAFETY_CODE) internal_error("uninitialised HT");
2019-02-04 22:26:45 +00:00
if (HT->analysis_hash[h] != NULL) {
hash_table_entry *hte = NULL;
LOOP_OVER_LINKED_LIST(hte, hash_table_entry, HT->analysis_hash[h]) {
2019-02-04 22:26:45 +00:00
if (Str::eq(hte->hash_key, text))
return hte;
}
2019-02-04 22:26:45 +00:00
}
if (create) {
hash_table_entry *hte = CREATE(hash_table_entry);
hte->language_reserved_word = 0;
2019-02-04 22:26:45 +00:00
hte->hash_key = Str::duplicate(text);
hte->usages = NEW_LINKED_LIST(hash_table_entry_usage);
hte->definition_line = NULL;
hte->as_function = NULL;
2019-02-04 22:26:45 +00:00
if (HT->analysis_hash[h] == NULL)
HT->analysis_hash[h] = NEW_LINKED_LIST(hash_table_entry);
ADD_TO_LINKED_LIST(hte, hash_table_entry, HT->analysis_hash[h]);
return hte;
}
return NULL;
}
2020-04-05 17:37:43 +00:00
hash_table_entry *Analyser::find_hash_entry_for_section(section *S, text_stream *text,
int create) {
return Analyser::find_hash_entry(&(S->sect_target->symbols), text, create);
}
2019-02-04 22:26:45 +00:00
@ Marking and testing these bits:
2024-03-09 06:17:52 +00:00
<<*>>=
2020-04-11 20:39:43 +00:00
hash_table_entry *Analyser::mark_reserved_word(hash_table *HT, text_stream *p, int e) {
2020-04-05 17:37:43 +00:00
hash_table_entry *hte = Analyser::find_hash_entry(HT, p, TRUE);
hte->language_reserved_word |= (1 << (e % 32));
2020-04-11 20:39:43 +00:00
hte->definition_line = NULL;
hte->as_function = NULL;
return hte;
2019-02-04 22:26:45 +00:00
}
2020-04-05 17:37:43 +00:00
void Analyser::mark_reserved_word_for_section(section *S, text_stream *p, int e) {
Analyser::mark_reserved_word(&(S->sect_target->symbols), p, e);
}
2020-04-11 20:39:43 +00:00
hash_table_entry *Analyser::mark_reserved_word_at_line(source_line *L, text_stream *p, int e) {
if (L == NULL) internal_error("no line for rw");
hash_table_entry *hte =
Analyser::mark_reserved_word(&(L->owning_section->sect_target->symbols), p, e);
hte->definition_line = L;
return hte;
}
2020-04-05 17:37:43 +00:00
int Analyser::is_reserved_word(hash_table *HT, text_stream *p, int e) {
hash_table_entry *hte = Analyser::find_hash_entry(HT, p, FALSE);
if ((hte) && (hte->language_reserved_word & (1 << (e % 32)))) return TRUE;
2019-02-04 22:26:45 +00:00
return FALSE;
}
2020-04-05 17:37:43 +00:00
int Analyser::is_reserved_word_for_section(section *S, text_stream *p, int e) {
return Analyser::is_reserved_word(&(S->sect_target->symbols), p, e);
}
2020-04-11 20:39:43 +00:00
source_line *Analyser::get_defn_line(section *S, text_stream *p, int e) {
hash_table_entry *hte = Analyser::find_hash_entry(&(S->sect_target->symbols), p, FALSE);
if ((hte) && (hte->language_reserved_word & (1 << (e % 32)))) return hte->definition_line;
2020-04-11 20:39:43 +00:00
return NULL;
}
language_function *Analyser::get_function(section *S, text_stream *p, int e) {
2020-04-11 20:39:43 +00:00
hash_table_entry *hte = Analyser::find_hash_entry(&(S->sect_target->symbols), p, FALSE);
if ((hte) && (hte->language_reserved_word & (1 << (e % 32)))) return hte->as_function;
2020-04-11 20:39:43 +00:00
return NULL;
}
2019-02-04 22:26:45 +00:00
@ Now we turn back to the actual analysis. When we spot an identifier that
we know, we record its usage with an instance of the following. Note that
each identifier can have at most one of these records per paragraph of code,
but that it can be used in multiple ways within that paragraph: for example,
a function might be both called and used as a constant value within the
same paragraph of code.
2024-03-09 06:17:52 +00:00
<<*>>=
2019-02-04 22:26:45 +00:00
typedef struct hash_table_entry_usage {
struct paragraph *usage_recorded_at;
2024-03-09 06:17:52 +00:00
int form_of_usage; /* bitmap of the [[*_USAGE]] constants defined above */
2020-05-09 12:05:00 +00:00
CLASS_DEFINITION
2019-02-04 22:26:45 +00:00
} hash_table_entry_usage;
@ And here's how we create these usages:
2024-03-09 06:17:52 +00:00
<<*>>=
2019-02-04 22:26:45 +00:00
void Analyser::analyse_find(web *W, source_line *L, text_stream *identifier, int u) {
2020-04-05 17:37:43 +00:00
hash_table_entry *hte =
Analyser::find_hash_entry_for_section(L->owning_section, identifier, FALSE);
2019-02-04 22:26:45 +00:00
if (hte == NULL) return;
hash_table_entry_usage *hteu = NULL, *loop = NULL;
LOOP_OVER_LINKED_LIST(loop, hash_table_entry_usage, hte->usages)
if (L->owning_paragraph == loop->usage_recorded_at) {
hteu = loop; break;
}
2019-02-04 22:26:45 +00:00
if (hteu == NULL) {
hteu = CREATE(hash_table_entry_usage);
hteu->form_of_usage = 0;
hteu->usage_recorded_at = L->owning_paragraph;
ADD_TO_LINKED_LIST(hteu, hash_table_entry_usage, hte->usages);
}
hteu->form_of_usage |= u;
}
2024-03-09 06:17:52 +00:00
@ \section{Open-source project support.}
2019-02-04 22:26:45 +00:00
The work here is all delegated. In each case we look for a script in the web's
folder: failing that, we fall back on a default script belonging to Inweb.
2024-03-09 06:17:52 +00:00
<<*>>=
2022-04-23 23:40:37 +00:00
void Analyser::write_makefile(web *W, filename *F, module_search *I, text_stream *platform) {
2022-04-21 23:06:45 +00:00
pathname *P = W->md->path_to_web;
text_stream *short_name = Pathnames::directory_name(P);
if ((Str::len(short_name) == 0) ||
(Str::eq(short_name, I".")) || (Str::eq(short_name, I"..")))
short_name = I"web";
TEMPORARY_TEXT(leafname)
WRITE_TO(leafname, "%S.mkscript", short_name);
filename *prototype = Filenames::in(P, leafname);
DISCARD_TEXT(leafname)
2019-02-04 22:26:45 +00:00
if (!(TextFiles::exists(prototype)))
2022-04-21 23:06:45 +00:00
prototype = Filenames::in(path_to_inweb_materials, I"default.mkscript");
2022-04-23 23:40:37 +00:00
Makefiles::write(W, prototype, F, I, platform);
2019-02-04 22:26:45 +00:00
}
void Analyser::write_gitignore(web *W, filename *F) {
2022-04-23 13:08:38 +00:00
pathname *P = W->md->path_to_web;
text_stream *short_name = Pathnames::directory_name(P);
if ((Str::len(short_name) == 0) ||
(Str::eq(short_name, I".")) || (Str::eq(short_name, I"..")))
short_name = I"web";
TEMPORARY_TEXT(leafname)
WRITE_TO(leafname, "%S.giscript", short_name);
filename *prototype = Filenames::in(P, leafname);
DISCARD_TEXT(leafname)
2019-02-04 22:26:45 +00:00
if (!(TextFiles::exists(prototype)))
2022-04-23 13:08:38 +00:00
prototype = Filenames::in(path_to_inweb_materials, I"default.giscript");
2019-02-04 22:26:45 +00:00
Git::write_gitignore(W, prototype, F);
}