fix: restore the rules the M7 half never had to implement
Unifying the two engines exposed what the split had been hiding: every rule that lived only in the M6 body was silently dropped for units the M7 half claimed, and since M1-M6 sources never reached that half, nothing failed until they all did. Eleven cases across m3, m5 and m6 caught it. Checker, all from the M6 statement and lvalue cases: - writing a struct field needs a writable place, so `p.x = 3` on a `let` is an error again (m3-badfield) - `let` cannot bind a mutable slice, a var with no initializer needs a type, and a void expression cannot initialize (m3-bad-mlet) - a returned reference must derive from a parameter or a static, and a void expression cannot be returned from a value function (m6-badarg, badret, badself, badtwo, badlocsl) - rebinding a reference must not outlive its source scope, and must release the previous borrow (m6-badscop) - the loop case delegates to the core, which carries the flow capture and merge that detects a value moved on every iteration; the M7 version had none of it (m5-bad-loop) Emitter: - builtins other than the print family (@size_of, @align_of) and the str alias methods are lowered by the core, which the M7 call path never reached, so they were emitted verbatim into the C (m3-struct) - the trim helper is emitted from the M7 type-helper pass as well, not only the core one, or the call has no definition to link (m6-oktrim) M1-M7 all green: 16, 19, 50, 20, 16, 57, 42.
This commit is contained in:
+48
-11
@@ -2247,6 +2247,12 @@ static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read)
|
||||
if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) {
|
||||
if (base->kind==FE_TYPE_REF && !base->ref_mut)
|
||||
err(s->c,n->loc,"cannot write through shared reference");
|
||||
/* Writing a field still needs a writable place. This branch used to
|
||||
be reached only by units mentioning M7 syntax, so it never had to
|
||||
repeat the check the M6 path does. */
|
||||
if (base->kind!=FE_TYPE_REF && base->kind!=FE_TYPE_OWNED &&
|
||||
!lvalue_writable(s,n->a))
|
||||
err(s->c,n->loc,"cannot assign through immutable value");
|
||||
field=fe_type_field(owner,n->b->text);
|
||||
if (!field) {
|
||||
err(s->c,n->loc,"assignment requires a valid struct field");
|
||||
@@ -2485,6 +2491,15 @@ static void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable)
|
||||
if (n->b && !fe_type_equal(expected,stored) &&
|
||||
!m7_actual_compatible(expected,stored,n->b))
|
||||
err(s->c,n->loc,"initializer type mismatch");
|
||||
/* Rules the M6 declaration case carried that this one has to repeat now
|
||||
that it is the only declaration case. */
|
||||
if (n->b && stored && stored->kind==FE_TYPE_VOID)
|
||||
err(s->c,n->loc,"void expression cannot initialize a variable");
|
||||
if (!mutable && expected && expected->kind==FE_TYPE_SLICE &&
|
||||
expected->ref_mut)
|
||||
err(s->c,n->loc,"let cannot bind a mutable slice");
|
||||
if (!n->b && !n->a)
|
||||
err(s->c,n->loc,"uninitialized var requires an explicit type");
|
||||
if (n->b) mark_moved(s,n->b,actual);
|
||||
initialized=n->b!=0;
|
||||
sym=add_symbol(s,s->scope,n->text,expected,0,mutable,initialized,
|
||||
@@ -2542,6 +2557,25 @@ static void check_stmt(FeCheckerState *s, FeNode *n)
|
||||
sym->initialized=1;
|
||||
fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc);
|
||||
sym->moved=sym->own.move;
|
||||
/* Rebinding a reference, from the M6 assignment case: the new
|
||||
source has to live at least as long as the reference does, and
|
||||
the previous borrow has to be released. */
|
||||
if (n->b && n->b->kind==FE_N_UNARY && n->b->text &&
|
||||
(strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0) &&
|
||||
fe_own_is_reference_like(sym->type)) {
|
||||
FeSym *root=own_root_symbol(s,n->b->a);
|
||||
if (root && root->owner!=sym->owner)
|
||||
err(s->c,n->b->loc,"reference would outlive its source scope");
|
||||
else if (root) {
|
||||
if (sym->borrow_root) {
|
||||
if (sym->borrow_mut)
|
||||
fe_own_release_exclusive(&sym->borrow_root->own);
|
||||
else fe_own_release_shared(&sym->borrow_root->own);
|
||||
}
|
||||
sym->borrow_root=root;
|
||||
sym->borrow_mut=strcmp(n->b->text,"&mut")==0;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FE_N_EXPR_STMT:
|
||||
@@ -2614,6 +2648,15 @@ static void check_stmt(FeCheckerState *s, FeNode *n)
|
||||
else
|
||||
stored=fe_type_intern(&s->c->types,"void");
|
||||
actual=n->a && n->a->sem_type ? n->a->sem_type : stored;
|
||||
/* R8, from the M6 return case: a returned reference has to come from a
|
||||
parameter or a static, never from a local. */
|
||||
if (expected && fe_own_is_reference_like(expected) &&
|
||||
!own_return_from_allowed_root(s,n->a))
|
||||
err(s->c,n->loc,
|
||||
"reference return must be derived from a parameter or static");
|
||||
if (n->a && stored && stored->kind==FE_TYPE_VOID &&
|
||||
expected && expected->kind!=FE_TYPE_VOID)
|
||||
err(s->c,n->loc,"void expression returned from value function");
|
||||
if (expected && expected->kind==FE_TYPE_ERROR_UNION && n->a &&
|
||||
actual && actual->kind==FE_TYPE_ERROR_UNION &&
|
||||
!fe_type_equal(expected,actual))
|
||||
@@ -2625,17 +2668,11 @@ static void check_stmt(FeCheckerState *s, FeNode *n)
|
||||
break;
|
||||
case FE_N_WHILE:
|
||||
case FE_N_FOR:
|
||||
/* One loop rule now that there is one checker: the body recurses
|
||||
through this function, so any expression in it is checked the same
|
||||
way whether or not the unit mentions optionals or error unions. */
|
||||
if (n->kind==FE_N_WHILE) {
|
||||
actual=check_expr(s,n->a);
|
||||
if (known(actual) && actual->kind!=FE_TYPE_BOOL)
|
||||
err(s->c,n->loc,"while condition must be bool");
|
||||
++s->loop_depth;
|
||||
check_stmt(s,n->b);
|
||||
--s->loop_depth;
|
||||
} else check_for(s,n);
|
||||
/* The core loop case carries the flow capture and merge that detects a
|
||||
value moved on every iteration, and it already recurses into the body
|
||||
through this function, so there is nothing to special-case here. The
|
||||
M7 half used to skip all of it. */
|
||||
check_stmt_core(s,n);
|
||||
break;
|
||||
case FE_N_BREAK:
|
||||
case FE_N_CONTINUE:
|
||||
|
||||
@@ -1580,6 +1580,14 @@ static void m7_emit_type_helpers(FeEmitter *e)
|
||||
m7_c_type(e,t),t->full_slicer,m7_c_type(e,t),t->slicer);
|
||||
fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,x.n); }\n",
|
||||
m7_c_type(e,t),t->tail_slicer,m7_c_type(e,t),t->slicer);
|
||||
if (node_uses_trim(e->check->ast->root) && !t->ref_mut) {
|
||||
fprintf(e->out,
|
||||
"static %s fe_trim_%s(%s s) { unsigned long a=0; unsigned long b=s.n;"
|
||||
" while (a<b && (s.p[a]==' '||s.p[a]=='\\t'||s.p[a]=='\\r'||s.p[a]=='\\n')) ++a;"
|
||||
" while (b>a && (s.p[b-1]==' '||s.p[b-1]=='\\t'||s.p[b-1]=='\\r'||s.p[b-1]=='\\n')) --b;"
|
||||
" return %s(s.p+a,b-a); }\n",
|
||||
t->cname,t->cname,t->cname,t->maker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1778,6 +1786,20 @@ static void m7_emit_call(FeEmitter *e, FeNode *n)
|
||||
emit_m4_builtin(e,n);
|
||||
return;
|
||||
}
|
||||
/* Every other builtin -- @size_of, @align_of and friends -- is lowered by
|
||||
the core emitter. Without this the generic path below emits the call
|
||||
verbatim, which is not C. */
|
||||
if (!n->a && n->text && n->text[0]=='@') {
|
||||
emit_expr_core(e,n);
|
||||
return;
|
||||
}
|
||||
/* Same for the built-in alias methods on str: the core emitter knows how to
|
||||
lower `line.trim()`, the generic member path would emit `.trim()`. */
|
||||
if (n->a && n->a->kind==FE_N_MEMBER && n->a->b && n->a->b->text &&
|
||||
strcmp(n->a->b->text,"trim")==0 && !n->children) {
|
||||
emit_expr_core(e,n);
|
||||
return;
|
||||
}
|
||||
if (n->a && n->a->kind==FE_N_MEMBER && n->a->a &&
|
||||
n->a->a->kind==FE_N_IDENT && n->a->a->text &&
|
||||
strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text &&
|
||||
|
||||
Reference in New Issue
Block a user