implement: unit identity -- dotted paths, name rules, source path

Units start here, with the part that needs no import graph: what a unit is
called and where it must live.

The parser only ever read a single identifier after `unit` and `import`, so
`unit game.main;` and `import std.io;` were syntax errors -- which is why the
dotted fixtures failed at the semicolon. It now reads a dotted path and stores
it canonically, dots included, since that spelling is the unit's identity
everywhere else. `import a.b as c;` parses too, with the alias on the node.

resolve.c is the new pass between parsing and checking, for the questions that
span files. It carries SPEC 8.1 so far: each path segment is ASCII lowercase,
starts with a letter, continues with letters, digits or underscore, and is at
most eight characters; and the dotted path must match the source path it was
read from, so game.world.map has to come from game/world/map.fe. The source
side is folded to lowercase before comparing, because a case-insensitive host
must not let two spellings become two units.

That rule then applied to the fixtures, which were not obeying it: 57 declared
a unit name unrelated to their file, left over from the milestone directories,
and eight had names too long to be legal. Both are now aligned -- the rule is
worth having only if the tree follows it.

units: badupper, badlong and unitbad pass. 138 -> 146 of 188. The rest of
units/ needs the import graph, which is the next piece: resolution, cycles,
bindings and visibility.
This commit is contained in:
2026-08-17 03:47:28 +09:00
parent 547d8c5ec2
commit 51e2568ba7
68 changed files with 215 additions and 61 deletions
+10
View File
@@ -1,5 +1,6 @@
#include "parser.h"
#include "check.h"
#include "resolve.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -80,6 +81,15 @@ int main(int argc, char **argv)
free(src);
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);
+37 -3
View File
@@ -302,12 +302,46 @@ static FeNode *statement(FeParser *p)
e=expr(p,0); if(is(p,FE_TOK_EQ)||is(p,FE_TOK_PLUS_EQ)||is(p,FE_TOK_MINUS_EQ)||is(p,FE_TOK_STAR_EQ)||is(p,FE_TOK_SLASH_EQ)||is(p,FE_TOK_PERCENT_EQ)||is(p,FE_TOK_AND_EQ)||is(p,FE_TOK_OR_EQ)||is(p,FE_TOK_XOR_EQ)||is(p,FE_TOK_SHL_EQ)||is(p,FE_TOK_SHR_EQ)){n=toknode(p,FE_N_ASSIGN,p->current);n->a=e;next(p);n->b=expr(p,0);}else{n=toknode(p,FE_N_EXPR_STMT,t);n->a=e;}want(p,FE_TOK_SEMI,"expected ';' after statement");return n;
}
/* A unit path is dotted: `game.world.map`. It is stored canonically, dots and
all, because that spelling is the unit's identity everywhere else. */
static char *unit_path(FeParser *p)
{
char buf[256];
unsigned long len=0;
if(!is_name(p)) return 0;
for(;;) {
unsigned long n=p->current.length;
if(len && len+1<sizeof buf) buf[len++]='.';
if(len+n>=sizeof buf){error(p,"unit path is too long");return 0;}
memcpy(buf+len,p->current.begin,n);
len+=n;
next(p);
if(!eat(p,FE_TOK_DOT)) break;
if(!is_name(p)){error(p,"expected a name after '.' in unit path");return 0;}
}
return fe_arena_strdup(&p->ast->arena,buf,len);
}
FeNode *fe_parse_unit(FeParser *p)
{
FeToken t=p->current, name; FeNode *root;
FeToken t=p->current; FeNode *root; char *path;
if(!eat(p,FE_TOK_UNIT)){error(p,"source must start with 'unit'");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"unit",4);}
root=toknode(p,FE_N_UNIT,t);if(is_name(p)){name=p->current;root->text=fe_arena_strdup(&p->ast->arena,name.begin,name.length);next(p);}else error(p,"expected unit name");want(p,FE_TOK_SEMI,"expected ';' after unit name");
while(eat(p,FE_TOK_IMPORT)){FeToken it=p->previous;FeNode *i=toknode(p,FE_N_IMPORT,it);if(is_name(p)){next(p);i->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected import name");want(p,FE_TOK_SEMI,"expected ';' after import");fe_node_add(root,i);}
root=toknode(p,FE_N_UNIT,t);
path=unit_path(p);
if(path) root->text=path; else error(p,"expected unit name");
want(p,FE_TOK_SEMI,"expected ';' after unit name");
while(eat(p,FE_TOK_IMPORT)){
FeToken it=p->previous;FeNode *i=toknode(p,FE_N_IMPORT,it);
path=unit_path(p);
if(path) i->text=path; else error(p,"expected import name");
/* `as` renames the binding; without it the binding is the last segment. */
if(eat(p,FE_TOK_AS)) {
if(is_name(p)){i->aux_text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);}
else error(p,"expected an alias name after 'as'");
}
want(p,FE_TOK_SEMI,"expected ';' after import");
fe_node_add(root,i);
}
while(!is(p,FE_TOK_EOF)){FeNode *d=decl(p);if(d)fe_node_add(root,d);}
return root;
}
+86
View File
@@ -0,0 +1,86 @@
#include "resolve.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] == '\\';
}
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;
}
+24
View File
@@ -0,0 +1,24 @@
#ifndef FE_RESOLVE_H
#define FE_RESOLVE_H
#include "ast.h"
#include "diag.h"
/* Unit-level resolution: identity, import bindings, and the unit graph.
This runs between parsing and semantic checking. It answers questions that
need more than one file -- what a unit is called, what it imports, and
whether those imports exist and terminate -- so that check.c can keep
looking at one function at a time. */
/* SPEC 8.1: each segment is ASCII lowercase, starts with a letter, continues
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. */
#define FE_UNIT_SEGMENT_MAX 8
/* 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
come from `game/world/map.fe`. `source_path` may be null to skip that half.
Returns non-zero when the unit is well formed. */
int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path);
#endif