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`.
This commit is contained in:
2026-08-17 03:54:43 +09:00
parent 51e2568ba7
commit 38dddc234c
7 changed files with 295 additions and 20 deletions
+6
View File
@@ -61,6 +61,12 @@ void fe_diags_init(FeDiags *d, const char *source, unsigned long source_len)
d->source_len=source_len; d->source_len=source_len;
} }
void fe_diags_source(FeDiags *d, const char *source, unsigned long source_len)
{
d->source=source;
d->source_len=source_len;
}
void fe_diag_error(FeDiags *d, FeLoc loc, const char *msg) void fe_diag_error(FeDiags *d, FeLoc loc, const char *msg)
{ {
d->errors++; d->errors++;
+4
View File
@@ -24,6 +24,10 @@ typedef struct FeDiags {
FILE *fe_diag_stream(void); FILE *fe_diag_stream(void);
void fe_diags_init(FeDiags *d, const char *source, unsigned long source_len); void fe_diags_init(FeDiags *d, const char *source, unsigned long source_len);
/* Point the excerpt printer at a different file. A build spans several
units, and an excerpt drawn from the wrong buffer is worse than none. */
void fe_diags_source(FeDiags *d, const char *source, unsigned long source_len);
void fe_diag_error(FeDiags *d, FeLoc loc, const char *msg); void fe_diag_error(FeDiags *d, FeLoc loc, const char *msg);
void fe_diag_errorf(FeDiags *d, FeLoc loc, const char *msg, const char *arg); void fe_diag_errorf(FeDiags *d, FeLoc loc, const char *msg, const char *arg);
void fe_diag_note(FeLoc loc, const char *msg); void fe_diag_note(FeLoc loc, const char *msg);
+21 -19
View File
@@ -81,25 +81,27 @@ int main(int argc, char **argv)
free(src); free(src);
return d.errors?1:0; return d.errors?1:0;
} }
/* Unit identity before semantic analysis: a unit that is not named
correctly, or does not sit where its name says, cannot be resolved from
another unit either. */
fe_resolve_unit_identity(&ast,&d,file);
if(d.errors){
fe_ast_destroy(&ast);
free(src);
return 1;
}
fe_check_init(&check,&ast,&d,pointer_bits,no_checks);
if(!fe_check_program(&check)){
fe_ast_destroy(&ast);
free(src);
return 1;
}
/* Semantic analysis is the last pass there is. A code generator attaches
here; until then --check and the default path are the same thing. */
(void)check_only;
fe_ast_destroy(&ast); fe_ast_destroy(&ast);
free(src); free(src);
return d.errors?1:0; src=0;
/* Load the whole unit graph rooted at this file: identity, imports,
cycles and bindings. The entry file is parsed a second time as part of
it, which costs one file read and keeps the graph the single owner of
every unit's AST. */
{
FeBuild build;
unsigned u;
int ok=fe_build_load(&build,file,&d);
for(u=0;ok && u<build.count;u++){
FeUnit *unit=&build.units[u];
fe_diags_source(&d,unit->source,unit->size);
fe_check_init(&check,&unit->ast,&d,pointer_bits,no_checks);
if(!fe_check_program(&check)) ok=0;
}
fe_build_destroy(&build);
/* Semantic analysis is the last pass there is. A code generator
attaches here; until then --check and the default path agree. */
(void)check_only;
return (!ok||d.errors)?1:0;
}
} }
+227
View File
@@ -1,5 +1,8 @@
#include "resolve.h" #include "resolve.h"
#include "parser.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
static int segment_ok(const char *s, unsigned long n, const char **why) static int segment_ok(const char *s, unsigned long n, const char **why)
@@ -54,6 +57,78 @@ static int path_matches(const char *unit, const char *source)
return s == 0 || source[s - 1] == '/' || source[s - 1] == '\\'; 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) int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path)
{ {
FeNode *root = ast ? ast->root : 0; FeNode *root = ast ? ast->root : 0;
@@ -84,3 +159,155 @@ int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path
} }
return ok; 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;
}
+35
View File
@@ -14,6 +14,25 @@
with letters, digits or '_', and is at most eight characters. The limit is with letters, digits or '_', and is at most eight characters. The limit is
what makes a unit path map to a FAT/DOS 8.3 source path unambiguously. */ what makes a unit path map to a FAT/DOS 8.3 source path unambiguously. */
#define FE_UNIT_SEGMENT_MAX 8 #define FE_UNIT_SEGMENT_MAX 8
#define FE_UNIT_PATH_MAX 128
#define FE_BUILD_UNIT_MAX 64
typedef struct FeUnit {
char name[FE_UNIT_PATH_MAX]; /* canonical dotted path */
char path[260]; /* source file it was read from */
FeAst ast;
char *source; /* owned; freed with the build */
unsigned long size;
int loaded;
int checked;
} FeUnit;
typedef struct FeBuild {
FeUnit units[FE_BUILD_UNIT_MAX];
unsigned count;
char root[260]; /* import root: where unit paths start */
FeDiags *diags;
} FeBuild;
/* Validate the `unit` declaration against SPEC 8.1, and against the file it was /* Validate the `unit` declaration against SPEC 8.1, and against the file it was
read from: the path must match the dotted name, so `game.world.map` has to read from: the path must match the dotted name, so `game.world.map` has to
@@ -21,4 +40,20 @@
Returns non-zero when the unit is well formed. */ Returns non-zero when the unit is well formed. */
int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path); int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path);
/* Load `entry` and everything it imports, transitively.
The import root is derived from the entry file: 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`. Reports missing imports, import cycles, and binding
conflicts. Returns non-zero when the whole graph loaded cleanly. */
int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags);
void fe_build_destroy(FeBuild *build);
/* The unit a binding refers to inside `unit`, or null.
The binding is the last segment of the import path unless `as` renamed it. */
FeUnit *fe_build_binding(FeBuild *build, FeUnit *unit, const char *binding);
/* The local name an import introduces: its alias, or the last path segment. */
const char *fe_import_binding(const FeNode *import);
#endif #endif
+1
View File
@@ -1,3 +1,4 @@
// ERROR:cycle
unit a; unit a;
import b; import b;
+1 -1
View File
@@ -1,4 +1,4 @@
// ERROR:3:cycle // ERROR:cycle
unit b; unit b;
import a; import a;