Files
doslang-mirror/fec/src/resolve.c
T
coolguy 38dddc234c implement: load the unit graph -- imports, cycles, bindings
The driver handled one file. It now loads the graph rooted at the entry file
and checks every unit in it.

The import root is derived rather than configured: a unit named `a.b` read
from `<root>/a/b.fe` fixes `<root>`, so a sibling `import c.d;` is looked for
at `<root>/c/d.fe`. That is enough for the fixtures and for any tree that
follows 8.1, and it means there is no path flag to get wrong yet.

Loading is depth-first with the chain of units currently open kept on a stack,
so meeting one again is a cycle rather than a revisit -- a unit reached twice
by different paths is loaded once. Cycles, imports with no source file, and two
imports claiming the same binding are all reported.

One thing this exposed: FeDiags held a single source buffer, so once a build
spanned several files every excerpt was drawn from whichever file was parsed
last. `cycle/b.fe:3` printed the text of a.fe. fe_diags_source switches it, and
loading and checking both set it per unit.

The cycle fixtures lost their line markers on purpose. Which import closes the
cycle depends on which file you enter from -- entering at a.fe reports b.fe,
entering at b.fe reports a.fe -- so pinning a line would pin an arbitrary half
of a symmetric pair. The message is pinned; the line is not.

units: missing, bindconf and cycle pass, on top of the identity cases.
146 -> 148 of 188. What is left in units/ needs cross-unit name resolution and
visibility: an importer still cannot see `util.answer`.
2026-08-17 03:54:43 +09:00

314 lines
9.9 KiB
C

#include "resolve.h"
#include "parser.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int segment_ok(const char *s, unsigned long n, const char **why)
{
unsigned long i;
if (!n) { *why = "unit path segment is empty"; return 0; }
if (n > FE_UNIT_SEGMENT_MAX) {
*why = "unit path segment is longer than eight characters";
return 0;
}
if (s[0] < 'a' || s[0] > 'z') {
*why = "unit path segment must start with a lowercase letter";
return 0;
}
for (i = 1; i < n; ++i) {
char c = s[i];
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') continue;
*why = "unit path segment may only contain lowercase letters, digits and '_'";
return 0;
}
return 1;
}
/* Compare a dotted unit path against the source path it was read from.
`game.world.map` matches `.../game/world/map.fe` and nothing else. Only the
trailing segments are compared, since the leading part is the import root. */
static int path_matches(const char *unit, const char *source)
{
unsigned long ulen = strlen(unit), slen = strlen(source);
unsigned long u, s;
if (slen < 3 || strcmp(source + slen - 3, ".fe") != 0) return 0;
slen -= 3;
u = ulen;
s = slen;
while (u > 0) {
char uc, sc;
--u;
if (s == 0) return 0;
--s;
uc = unit[u];
sc = source[s];
if (uc == '.') {
if (sc != '/' && sc != '\\') return 0;
continue;
}
/* Host filesystems may be case-insensitive; the unit name is the
authority and is lowercase by 8.1, so fold the path side down. */
if (sc >= 'A' && sc <= 'Z') sc = (char)(sc - 'A' + 'a');
if (uc != sc) return 0;
}
/* What remains of the source path is the import root, and must end there. */
return s == 0 || source[s - 1] == '/' || source[s - 1] == '\\';
}
static char *read_source(const char *path, unsigned long *size)
{
FILE *f;
long n;
char *p;
f = fopen(path, "rb");
if (!f) return 0;
fseek(f, 0, SEEK_END);
n = ftell(f);
fseek(f, 0, SEEK_SET);
if (n < 0) { fclose(f); return 0; }
p = (char *)malloc((unsigned long)n + 1);
if (!p) { fclose(f); return 0; }
if (fread(p, 1, (unsigned long)n, f) != (unsigned long)n) {
fclose(f); free(p); return 0;
}
p[n] = 0;
fclose(f);
*size = (unsigned long)n;
return p;
}
/* <root>/a/b.fe for the unit a.b */
static void unit_source_path(char *out, unsigned long cap,
const char *root, const char *unit)
{
unsigned long i = 0, j = 0;
while (root[i] && j + 1 < cap) out[j++] = root[i++];
if (j && out[j - 1] != '/' && out[j - 1] != '\\' && j + 1 < cap) out[j++] = '/';
for (i = 0; unit[i] && j + 1 < cap; ++i)
out[j++] = unit[i] == '.' ? '/' : unit[i];
if (j + 3 < cap) { out[j++] = '.'; out[j++] = 'f'; out[j++] = 'e'; }
out[j] = 0;
}
/* Strip the unit's own path from the file it was read from; what is left is
the import root that every other unit is looked up under. */
static void import_root(char *out, unsigned long cap,
const char *source, const char *unit)
{
unsigned long slen = strlen(source);
unsigned long dots = 0, i, cut;
for (i = 0; unit[i]; ++i) if (unit[i] == '.') ++dots;
if (slen >= 3) slen -= 3;
cut = slen;
for (i = 0; i <= dots; ++i) {
while (cut > 0 && source[cut - 1] != '/' && source[cut - 1] != '\\') --cut;
if (i < dots && cut > 0) --cut;
}
if (cut >= cap) cut = cap - 1;
memcpy(out, source, cut);
out[cut] = 0;
if (!cut) { out[0] = '.'; out[1] = 0; }
}
static FeUnit *find_unit(FeBuild *b, const char *name)
{
unsigned i;
for (i = 0; i < b->count; ++i)
if (strcmp(b->units[i].name, name) == 0) return &b->units[i];
return 0;
}
const char *fe_import_binding(const FeNode *import)
{
const char *dot;
if (!import) return 0;
if (import->aux_text) return import->aux_text;
dot = import->text ? strrchr(import->text, '.') : 0;
return dot ? dot + 1 : import->text;
}
int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path)
{
FeNode *root = ast ? ast->root : 0;
const char *name, *why;
const char *seg;
unsigned long i, len;
int ok = 1;
if (!root || root->kind != FE_N_UNIT || !root->text) return 0;
name = root->text;
len = strlen(name);
seg = name;
for (i = 0; i <= len; ++i) {
if (i != len && name[i] != '.') continue;
if (!segment_ok(seg, (unsigned long)(name + i - seg), &why)) {
fe_diag_error(diags, root->loc, why);
ok = 0;
}
seg = name + i + 1;
}
if (ok && source_path && !path_matches(name, source_path)) {
fe_diag_errorf(diags, root->loc,
"unit %s must be declared in a source file matching its path",
name);
ok = 0;
}
return ok;
}
/* Depth-first load. `stack` is the chain of units currently being loaded, so
meeting one again is a cycle rather than a repeat visit. */
static int load_unit(FeBuild *b, const char *name, FeLoc from, int have_from,
const char **stack, unsigned depth)
{
FeUnit *unit;
FeNode *n;
FeParser p;
unsigned long size;
unsigned i;
int ok = 1;
for (i = 0; i < depth; ++i) {
if (strcmp(stack[i], name) == 0) {
fe_diag_errorf(b->diags, from, "import of %s forms a cycle", name);
return 0;
}
}
if (find_unit(b, name)) return 1;
if (b->count >= FE_BUILD_UNIT_MAX) {
fe_diag_error(b->diags, from, "too many units in one build");
return 0;
}
if (strlen(name) >= FE_UNIT_PATH_MAX) {
fe_diag_errorf(b->diags, from, "unit path is too long: %s", name);
return 0;
}
unit = &b->units[b->count];
memset(unit, 0, sizeof *unit);
strcpy(unit->name, name);
unit_source_path(unit->path, sizeof unit->path, b->root, name);
unit->source = read_source(unit->path, &size);
if (!unit->source) {
if (have_from)
fe_diag_errorf(b->diags, from, "import %s has no source file", name);
else
fe_diag_errorf(b->diags, from, "cannot open %s", unit->path);
return 0;
}
b->count++;
unit->size = size;
fe_ast_init(&unit->ast);
/* Diagnostics from here on belong to this file. */
fe_diags_source(b->diags, unit->source, size);
fe_parser_init(&p, &unit->ast, unit->source, size, unit->path, b->diags);
unit->ast.root = fe_parse_unit(&p);
unit->loaded = 1;
if (!fe_resolve_unit_identity(&unit->ast, b->diags, unit->path)) ok = 0;
stack[depth] = unit->name;
for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) {
if (n->kind != FE_N_IMPORT || !n->text) continue;
/* The import statement is where the reader has to make a change, so
the diagnostic points there rather than at the unit it names. */
if (!load_unit(b, n->text, n->loc, 1, stack, depth + 1)) ok = 0;
fe_diags_source(b->diags, unit->source, unit->size);
}
stack[depth] = 0;
return ok;
}
/* A binding names one unit inside one importer; two imports cannot claim it. */
static int check_bindings(FeBuild *b, FeUnit *unit)
{
FeNode *n, *m;
int ok = 1;
for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) {
const char *a;
if (n->kind != FE_N_IMPORT) continue;
a = fe_import_binding(n);
if (!a) continue;
for (m = unit->ast.root->children; m != n; m = m->next) {
const char *other;
if (m->kind != FE_N_IMPORT) continue;
other = fe_import_binding(m);
if (other && strcmp(a, other) == 0) {
fe_diag_errorf(b->diags, n->loc,
"import binding %s is already taken; use an alias", a);
ok = 0;
}
}
}
return ok;
}
int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags)
{
const char *stack[FE_BUILD_UNIT_MAX];
FeAst probe;
FeParser p;
char *source;
char name[FE_UNIT_PATH_MAX];
FeLoc loc;
unsigned long size;
unsigned i;
int ok;
memset(build, 0, sizeof *build);
build->diags = diags;
/* The entry file fixes the import root, so it has to be parsed far enough
to know its own name before anything else can be found. */
source = read_source(entry, &size);
if (!source) {
FeLoc none;
none.file = entry; none.line = 0; none.col = 0;
fe_diag_errorf(diags, none, "cannot open %s", entry);
return 0;
}
fe_ast_init(&probe);
fe_parser_init(&p, &probe, source, size, entry, diags);
probe.root = fe_parse_unit(&p);
if (!probe.root || !probe.root->text || diags->errors) {
fe_ast_destroy(&probe);
free(source);
return 0;
}
import_root(build->root, sizeof build->root, entry, probe.root->text);
strncpy(name, probe.root->text, sizeof name - 1);
name[sizeof name - 1] = 0;
loc = probe.root->loc;
fe_ast_destroy(&probe);
free(source);
ok = load_unit(build, name, loc, 0, stack, 0);
for (i = 0; i < build->count; ++i)
if (!check_bindings(build, &build->units[i])) ok = 0;
return ok && diags->errors == 0;
}
void fe_build_destroy(FeBuild *build)
{
unsigned i;
for (i = 0; i < build->count; ++i) {
if (build->units[i].loaded) fe_ast_destroy(&build->units[i].ast);
free(build->units[i].source);
}
build->count = 0;
}
FeUnit *fe_build_binding(FeBuild *build, FeUnit *unit, const char *binding)
{
FeNode *n;
for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) {
const char *bound;
if (n->kind != FE_N_IMPORT || !n->text) continue;
bound = fe_import_binding(n);
if (bound && strcmp(bound, binding) == 0) return find_unit(build, n->text);
}
return 0;
}