audit: SPEC-파서 괴리 일곱을 정리한다

구현 셋, SPEC 다섯. 어느 쪽이 틀렸는지는 항목마다 따로 판정했다.

구현:
- ~ 를 넣었다. ^ 는 xor 과 .^ 가 가져가서 비트 NOT 이 자기 철자를 못 갖고
  있었다. 렉서·파서·검사·lowering(전부 1 과의 xor).
- 전역 static/var 는 타입을 적는다. 다른 유닛이 읽는 링커 심볼이라 초기값의
  생김새에 타입을 맡기면 그쪽이 보는 것이 달라진다. const 는 그대로 추론한다.
- error code 는 정수 리터럴 하나다. 식이면 중복도 예약된 0 도 검사할 수 없다.

SPEC:
- 최상위 comptime if 를 뺐다. comptime 조건은 타입 술어뿐인데(§7.5) 유닛
  바깥에는 바인딩된 타입 파라미터가 없어 물어볼 것이 없다. §11 v0.2.
- 타입 이름은 [binding.]Name 이다. import 가 마지막 segment 를 바인딩하므로
  점 둘 이상은 만들어질 수 없는데 문법이 unit_path 를 쓰고 있었다.
- catch 의 EBNF 가 §4.6 보다 넓었다. 값을 주는 짧은 형태와 에러를 받는 블록
  형태 둘로 나눠 적었다.
- | 와 ^ 를 한 단계로 둔 것을 쪼갰다. 합치면 a | b ^ c 가 좌결합으로
  (a|b)^c 가 되어 C 에서 온 사람을 속인다. 구현이 C 순서로 옳았다.
- 마지막 필드 쉼표 생략을 명세에 적었다. enum 은 이미 허용하고 있었다.

그리고 tests/run.py 가 마커를 진단 스트림에만 맞춘다. --dump-ast 모드에서는
AST 덤프가 stdout 으로 먼저 나와서 parse/ fixture 는 마커를 쓸 수 없었다.

249/249, 39/39.
This commit is contained in:
2026-08-17 16:52:12 +09:00
parent 6dc298d828
commit 730bcac282
13 changed files with 145 additions and 24 deletions
+14
View File
@@ -11,6 +11,7 @@ DOS/Windows용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`.
| `IR.md` | 중간 표현. 프론트엔드와 기계 사이 |
| `TODO.md` | 남은 작업과 정해진 것. 언어 규칙은 `SPEC.md` 를 가리키기만 한다 |
| `fec/tests/*/README.md` | 각 fixture 디렉터리가 무엇을 검사하는지 |
| `audits/<날짜>-<주제>.md` | 그때 조사해보니 어땠는지. 불변 기록 |
## 파이프라인
@@ -59,6 +60,19 @@ uv run python tests/build.py <프로그램.fe> # 하나만 빌드해서 돌려
줄 범위로 자르고 -- 주제별로 묶는 것보다 정확하다, 한 줄도 잃거나 겹치지 않으니 --
공유하는 것은 비공개 헤더(`checkpri.h`, `lowerpri.h`)에 모은다.
## 조사 기록
한 번 조사하고 끝나는 것 -- 명세와 구현의 대조, 진단 증거 수집, 외부 감사 --
`audits/<날짜>-<주제>.md`에 남긴다. 날짜와 기준 커밋을 적는다. 조사에서
나온 **결론**은 `SPEC.md``TODO.md`로 옮기고, audit 자체는 그때 무엇을
봤는지의 기록으로 둔다.
본문은 고치지 않는다. 다만 해결되면 맨 위에 **해결 줄 하나**를 붙인다 --
어느 커밋에서 어떻게 정리됐는지. 그것 없이는 읽는 사람이 아직 살아있는
문제인지 알 수 없다.
계획 문서는 두지 않는다 -- 끝난 계획은 git log 다.
## 작업 흐름
- 명세 판단이 바뀌면 `SPEC.md`를 즉시 갱신한다. 구현이 명세와 다르면 둘 중 하나가
+32 -18
View File
@@ -333,20 +333,21 @@ unit_path := ident ('.' ident)*
import := 'import' unit_path ['as' ident] ';'
decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl
| const_decl | global_decl) | comptime_decl
comptime_decl := 'comptime' 'if' expr '{' decl* '}' ['else' ('{' decl* '}' | comptime_decl)]
| const_decl | global_decl)
fn_decl := ['extern' string] 'fn' ident
'(' [param (',' param)*] ')' ['->' type] (block | ';')
param := ['comptime'] ident ':' type
generic_params := '(' ident (',' ident)* ')'
struct_decl := ['packed'] 'struct' ident [generic_params] '{' member* '}'
// field의 마지막 쉼표는 '}' 바로 앞에서 생략할 수 있다
member := ['pub'] (field | fn_decl)
field := ident ':' type ','
enum_decl := 'enum' ident [generic_params] '{' variant (',' variant)* [','] '}'
variant := ident | ident '(' type ')' | ident '{' vfield* '}'
vfield := ident ':' type ','
error_decl := 'error' ident '{' ident '=' int (',' ident '=' int)* [','] '}'
error_decl := 'error' ident '{' ident '=' int_literal
(',' ident '=' int_literal)* [','] '}'
const_decl := 'const' ident [':' type] '=' expr ';'
global_decl := 'static' ident ':' type '=' expr ';'
| 'var' ident ':' type '=' expr ';'
@@ -378,21 +379,30 @@ pattern := ident // 배리언트, 페이로드 없음
| 'Some' '(' ident ')' | 'None'
| int_literal | char_literal | 'true' | 'false' | '_'
qualified_name := ident ('.' ident)*
type := qualified_name
| '?' type | '!' type | qualified_name '!' type
type_name := [ident '.'] ident // [binding '.'] Name
type := type_name
| '?' type | '!' type | type_name '!' type
| '^' type | '&' ['mut'] type | '*' type
| '[' expr ']' type | '[' ']' ['mut'] type
| 'fn' '(' [type (',' type)*] ')' ['->' type]
| qualified_name '(' type (',' type)* ')' // 제네릭 인스턴스
| type_name '(' type (',' type)* ')' // 제네릭 인스턴스
catch_expr := expr 'catch' ['|' ident '|'] (expr | block)
catch_expr := expr 'catch' expr | expr 'catch' '|' ident '|' block
orelse_expr := expr 'orelse' expr
```
`member``pub`은 필드와 메서드 모두에 개별로 붙는다(§8). 필드와 메서드는 순서를
섞어 쓸 수 있다. `catch`블록 형태는 값을 만들지 않으며 §4.6의 규칙을 따른다.
`unit_path` segment의 lexical 제한과 source path 대응은 §8.1이 규정한다.
섞어 쓸 수 있다. `catch` 형태는 서로 다른 일을 한다: 값을 주는 짧은 형태와,
에러를 받아 빠져나가는 블록 형태다. 블록은 값을 만들지 않으므로(§11) 바인딩이
있는 쪽만 블록을 받는다(§4.6). `unit_path` segment의 lexical 제한과 source path
대응은 §8.1이 규정한다.
타입 이름은 `[binding '.'] Name`이다. `import`는 unit path의 **마지막 segment**를
바인딩하므로(§8.2) 점이 둘 이상인 타입 이름은 만들어질 수 없다. `unit_path`
자체는 `import``unit` 선언에서만 쓴다.
전역 `static`/`var`는 타입을 적는다. 다른 유닛이 읽는 링커 심볼이라 초기값의
생김새에 타입을 맡기면 그쪽이 보는 것이 달라진다. `const`는 추론한다.
### 6.2 표현식 우선순위 (낮음 → 높음)
@@ -405,20 +415,23 @@ orelse_expr := expr 'orelse' expr
2 or
3 and
4 == != < <= > >=
5 | ^
6 &
7 << >>
8 + - +% -%
9 * / % *%
10 단항: - not ~ & &mut try
11 후위: .field .? .^ [i] [a..b] (args) as T
12 기본: literal, ident, '(' expr ')', struct_literal, @builtin(...)
5 |
6 ^
7 &
8 << >>
9 + - +% -%
10 * / % *%
11 단항: - not ~ & &mut try
12 후위: .field .? .^ [i] [a..b] (args) as T
13 기본: literal, ident, '(' expr ')', struct_literal, @builtin(...)
```
- `and`, `or`는 단축 평가한다. 좌변이 답을 정하면 우변을 평가하지 않는다.
- `orelse``catch`도 lazy다. 좌변이 각각 `Some`/success이면 우변 또는 handler를
평가하지 않는다(§4.5·§4.6).
- `as`는 후위 우선순위(단항보다 강함)지만 단항 연산자 바로 뒤에 `as`가 나타나면 모호한 비용을 숨기지 않도록 괄호를 강제한다. `(-x) as u32``-(x as u32)`는 허용하고 `-x as u32`는 컴파일 에러다.
- `|`, `^`, `&`는 서로 다른 단계다. C와 같은 순서이며, 한 단계로 합치면
`a | b ^ c`가 좌결합으로 `(a | b) ^ c`가 되어 C에서 온 사람을 속인다.
- `..`는 일반 표현식 연산자가 아니며 `for` 헤더에서만 쓸 수 있다.
- 비교 연산 체이닝 금지(`a < b < c`는 에러).
- `.field`, `[i]`, `[a..b]`, 메서드 호출은 `&`, `&mut`, `^`를 필요한 만큼 자동 projection한다. 값 자체의 역참조는 `.^`가 필요하며 raw `*T`와 optional `?T`는 자동 역참조하지 않는다.
@@ -791,6 +804,7 @@ binding은 마지막 segment라 `io.write`, `mem.replace` 형태로 사용한다
| GC | 영구 | 결정적 비용 원칙 위반 | 소유권 + RAII + 아레나 |
| 암묵 형변환 | 영구 | 버그 원인 1위 | `as` |
| 상속 | 영구 | 숨은 vtable, 취약한 기반 클래스 | 합성 |
| 최상위 `comptime if` | v0.2 | 타깃이 하나이고 comptime 조건은 타입 술어뿐이라(§7.5) 유닛 바깥에는 물어볼 것이 없다 | 함수 안의 `comptime if`, 또는 유닛을 나눈다 |
### 11.1 인터페이스 설계 스케치 (v0.2 예정)
+19
View File
@@ -3,6 +3,8 @@
- 날짜: 2026-08-17
- 기준 커밋: `52aaff62e490e37a0995aaaf7cbda47cf98e54a7`
- 범위: `SPEC.md` §6과 `fec/src/lexer.c`, `fec/src/parser.c`
- **해결: 일곱 전부. 구현 셋(PARSE-04a·05·06), SPEC 다섯(01·02·03·04b·07).**
판정과 근거는 아래 표에 덧붙였다. 본문은 조사 시점 그대로다.
## 현재 문제
@@ -16,6 +18,23 @@
| PARSE-06 | error code는 정수 literal | 일반 expression을 파싱하며 literal이 아니면 code 검증을 건너뜀 | `error E { Bad = 1 + 2, }`가 검사 통과 |
| PARSE-07 | struct field와 enum vfield의 쉼표 필수 | 닫는 `}` 바로 앞에서는 쉼표 생략 허용 | `struct S { x: i32 }`가 검사 통과 |
## 해결
| ID | 어느 쪽이 틀렸나 | 무엇을 했나 |
|---|---|---|
| PARSE-01 | SPEC | comptime 조건은 타입 술어뿐이라(§7.5) 유닛 바깥에는 물어볼 것이 없다. `comptime_decl``decl` 에서 빼고 §11 v0.2 로 |
| PARSE-02 | SPEC | `import` 는 unit path 의 마지막 segment 를 바인딩하므로 점 둘 이상인 타입 이름은 만들어질 수 없다. 문법을 `type_name := [ident '.'] ident` 로 |
| PARSE-03 | SPEC | §4.6 과 §11(블록 표현식 배제)이 실제 규칙이고 EBNF 가 넓었다. 두 형태로 나눠 적었다 |
| PARSE-04a | 구현 | `~` 를 넣었다. 렉서·파서·검사·lowering(`xor` with all ones) |
| PARSE-04b | SPEC | `\|``^` 를 한 단계로 두면 `a \| b ^ c``(a\|b)^c` 가 되어 C 에서 온 사람을 속인다. 구현(C 순서)이 옳아서 표를 쪼갰다 |
| PARSE-05 | 구현 | 전역 `static`/`var` 는 타입 필수. `const` 는 그대로 추론 |
| PARSE-06 | 구현 | error code 는 정수 리터럴 하나만 받는다 |
| PARSE-07 | SPEC | 마지막 쉼표 생략은 흔하고 `enum` 은 이미 허용하고 있었다. 명세에 적었다 |
fixture: `parse/badgtype.fe`, `parse/badecode.fe`, `parse/okglobal.fe`,
`exec/bitnot.fe`. 그리고 `tests/run.py` 가 마커를 진단 스트림에만 맞춘다 --
`--dump-ast` 모드에서 AST 덤프가 먼저 나와 마커가 못 쓰이고 있었다.
## 검증
- `uv run python tests/run.py`: `240/240` 통과
+5
View File
@@ -371,6 +371,11 @@ FeType *check_expr_core(FeCheckerState *s, FeNode *n)
} else if (strcmp(op, "-") == 0) {
if (known(a) && !fe_type_is_integer(a))
err(c, n->loc, "unary '-' requires integer");
} else if (strcmp(op, "~") == 0) {
/* Flipping every bit only means something where the bits are the
value (SPEC 6.2). */
if (known(a) && !fe_type_is_integer(a))
err(c, n->loc, "unary '~' requires integer");
} else if (strcmp(op, "try") == 0) {
/* SPEC 6.4: try is only allowed inside a function returning an error
union. Checked on the expression rather than on the statement so
+2
View File
@@ -145,6 +145,7 @@ FeToken fe_lexer_next(FeLexer *l)
case '&': if(cur(l)=='&'){advance(l);fe_diag_error(l->diags,here(l,line,col),"&& is not a Ferro logical operator; use 'and'");return tok(l,FE_TOK_UNKNOWN,start,line,col);} if(cur(l)=='='){advance(l);return tok(l,FE_TOK_AND_EQ,start,line,col);} return tok(l,FE_TOK_AND,start,line,col);
case '|': if(cur(l)=='|'){advance(l);fe_diag_error(l->diags,here(l,line,col),"|| is not a Ferro logical operator; use 'or'");return tok(l,FE_TOK_UNKNOWN,start,line,col);} if(cur(l)=='='){advance(l);return tok(l,FE_TOK_OR_EQ,start,line,col);} return tok(l,FE_TOK_OR,start,line,col);
case '^': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_XOR_EQ,start,line,col);} return tok(l,FE_TOK_XOR,start,line,col);
case '~': return tok(l,FE_TOK_TILDE,start,line,col);
default: fe_diag_error(l->diags,here(l,line,col),"unknown character"); return tok(l,FE_TOK_UNKNOWN,start,line,col);
}
}
@@ -162,6 +163,7 @@ const char *fe_token_name(FeTokKind k)
case FE_TOK_TRUE:return "true"; case FE_TOK_FALSE:return "false"; case FE_TOK_NULL:return "null";
case FE_TOK_UNDEFINED:return "undefined"; case FE_TOK_AND_KW:return "and"; case FE_TOK_OR_KW:return "or";
case FE_TOK_NOT:return "not"; case FE_TOK_BANG:return "!";
case FE_TOK_TILDE:return "~";
case FE_TOK_LBRACE:return "{"; case FE_TOK_RBRACE:return "}"; case FE_TOK_LPAREN:return "("; case FE_TOK_RPAREN:return ")";
case FE_TOK_SEMI:return ";"; case FE_TOK_COLON:return ":"; case FE_TOK_COMMA:return ",";
case FE_TOK_EQ:return "="; case FE_TOK_ARROW:return "->"; case FE_TOK_FATARROW:return "=>";
+1 -1
View File
@@ -20,7 +20,7 @@ typedef enum FeTokKind {
FE_TOK_PLUS_EQ, FE_TOK_MINUS_EQ, FE_TOK_STAR_EQ, FE_TOK_SLASH_EQ, FE_TOK_PERCENT_EQ,
FE_TOK_PLUS_WRAP, FE_TOK_MINUS_WRAP, FE_TOK_STAR_WRAP,
FE_TOK_EQ, FE_TOK_EQEQ, FE_TOK_NE, FE_TOK_LT, FE_TOK_LE, FE_TOK_GT, FE_TOK_GE,
FE_TOK_AND, FE_TOK_OR, FE_TOK_AND_KW, FE_TOK_OR_KW, FE_TOK_XOR, FE_TOK_NOT, FE_TOK_BANG, FE_TOK_SHL, FE_TOK_SHR,
FE_TOK_AND, FE_TOK_OR, FE_TOK_AND_KW, FE_TOK_OR_KW, FE_TOK_XOR, FE_TOK_TILDE, FE_TOK_NOT, FE_TOK_BANG, FE_TOK_SHL, FE_TOK_SHR,
FE_TOK_AND_EQ, FE_TOK_OR_EQ, FE_TOK_XOR_EQ, FE_TOK_SHL_EQ, FE_TOK_SHR_EQ,
FE_TOK_ANDAND, FE_TOK_OROR, FE_TOK_ARROW, FE_TOK_FATARROW, FE_TOK_AT,
FE_TOK_QUESTION, FE_TOK_UNKNOWN
+7
View File
@@ -117,6 +117,13 @@ Slot lower_expr_core(Lower *L, FeNode *n)
return slot_value(fe_ir_binary(L->m, L->b, FE_IR_SUB, it, zero, v,
0), it);
}
if (n->text && !strcmp(n->text, "~")) {
/* Every bit flipped is every bit exchanged with a one. */
unsigned ones = fe_ir_const(L->m, L->b, it, -1L);
unsigned v = as_value(L, lower_expr(L, n->a), n->a);
return slot_value(fe_ir_binary(L->m, L->b, FE_IR_XOR, it, v, ones,
0), it);
}
if (n->text && !strcmp(n->text, "not")) {
unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0);
unsigned v = as_value(L, lower_expr(L, n->a), n->a);
+3 -3
View File
@@ -167,7 +167,7 @@ static FeNode *postfix(FeParser *p)
static FeNode *expr(FeParser *p, int minprec)
{
FeToken t=p->current; FeNode *left,*n; int prec;
if(is(p,FE_TOK_MINUS)||is(p,FE_TOK_NOT)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)||is(p,FE_TOK_STAR)||is(p,FE_TOK_TRY)) { next(p); n=toknode(p,FE_N_UNARY,t); if(t.kind==FE_TOK_AND && eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"&mut",4); n->a=expr(p,11); left=n; }
if(is(p,FE_TOK_MINUS)||is(p,FE_TOK_NOT)||is(p,FE_TOK_TILDE)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)||is(p,FE_TOK_STAR)||is(p,FE_TOK_TRY)) { next(p); n=toknode(p,FE_N_UNARY,t); if(t.kind==FE_TOK_AND && eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"&mut",4); n->a=expr(p,11); left=n; }
else left=postfix(p);
for(;;) { t=p->current;prec=precedence(t.kind);if(prec<=minprec)break;next(p);n=toknode(p,FE_N_BINARY,t);n->a=left;if(t.kind==FE_TOK_CATCH && eat(p,FE_TOK_OR)){if(is_name(p))n->b=toknode(p,FE_N_IDENT,p->current),next(p);else error(p,"expected catch binding");want(p,FE_TOK_OR,"expected '|' after catch binding");n->c=block(p);}else n->b=expr(p,prec);left=n; }
return left;
@@ -228,9 +228,9 @@ static FeNode *decl(FeParser *p)
if(eat(p,FE_TOK_PACKED)) t=p->previous;
if(eat(p,FE_TOK_STRUCT)) { n=toknode(p,FE_N_STRUCT,t);if(pub)n->flags|=FE_NODE_PUB;if(t.kind==FE_TOK_PACKED)n->flags|=FE_NODE_PACKED;if(!is_name(p)){error(p,"expected struct name");return n;}next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);if(eat(p,FE_TOK_LPAREN)){n->a=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"generics",8);while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n->a,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in struct");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){int mpub=eat(p,FE_TOK_PUB);if(is(p,FE_TOK_FN))fe_node_add(n,fn_decl(p,mpub,0,0,0));else fe_node_add(n,field(p,mpub));}want(p,FE_TOK_RBRACE,"expected '}' after struct");return n; }
if(eat(p,FE_TOK_ENUM)) { n=toknode(p,FE_N_ENUM,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected enum name");if(eat(p,FE_TOK_LPAREN)){n->a=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"generics",8);while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n->a,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in enum");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected variant name");recover(p);break;}if(eat(p,FE_TOK_LPAREN)){v->a=type(p);want(p,FE_TOK_RPAREN,"expected ')' in variant");}else if(eat(p,FE_TOK_LBRACE)){while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))fe_node_add(v,field(p,1));want(p,FE_TOK_RBRACE,"expected '}' in variant");}fe_node_add(n,v);if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RBRACE,"expected '}' after enum");return n; }
if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");v->a=expr(p,0);want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; }
if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");if(is(p,FE_TOK_INT)){v->a=toknode(p,FE_N_LITERAL,p->current);next(p);}else error(p,"an error code must be an integer literal");if(!is(p,FE_TOK_COMMA)&&!is(p,FE_TOK_RBRACE)){error(p,"an error code must be an integer literal");recover(p);break;}want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; }
if(eat(p,FE_TOK_SHARED)) { shared=1; if(eat(p,FE_TOK_ATOMIC)) atomic=1; if(!is(p,FE_TOK_VAR)) error(p,"expected 'var' after shared"); }
if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(pub)n->flags|=FE_NODE_PUB;if(kk==FE_TOK_STATIC)n->flags|=FE_NODE_STATIC;if(shared)n->flags|=FE_NODE_SHARED;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; }
if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(pub)n->flags|=FE_NODE_PUB;if(kk==FE_TOK_STATIC)n->flags|=FE_NODE_STATIC;if(shared)n->flags|=FE_NODE_SHARED;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);else if(kk!=FE_TOK_CONST)error(p,"a global declaration requires an explicit type");want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; }
error(p,"expected declaration"); before=p->current.kind; recover(p);
if (p->current.kind==before && p->current.kind!=FE_TOK_EOF) next(p);
return 0;
+37
View File
@@ -0,0 +1,37 @@
// EXIT:0
// OUTPUT:i32 -1 -6 4
// OUTPUT:u8 255 250
// OUTPUT:mask 240 15
// OUTPUT:prec 7 3
unit bitnot;
import std.io;
// `~` flips every bit (SPEC §6.2, 단항). `^` is taken by xor and by `.^`, so
// bitwise NOT needs its own spelling.
//
// `|`, `^` and `&` are three different levels, in C's order. Merging `|` and
// `^` would make `a | b ^ c` bind as `(a | b) ^ c`, which is not what anyone
// coming from C reads it as.
fn main() -> i32 {
let a: i32 = 0;
let b: i32 = 5;
let c: i32 = -5;
@print("i32 {} {} {}\n", ~a, ~b, ~c);
let u: u8 = 0;
let v: u8 = 5;
@print("u8 {} {}\n", (~u) as i32, (~v) as i32);
// Clearing bits is what the operator is for.
let bits: u8 = 255;
let low: u8 = 15;
@print("mask {} {}\n", (bits & ~low) as i32, (bits & low) as i32);
// 1 | 2 ^ 4 is 1 | (2 ^ 4) = 1 | 6 = 7, not (1 | 2) ^ 4 = 3 ^ 4 = 7.
// Those agree, so pick operands that do not: 3 | 1 ^ 2 is 3 | 3 = 3,
// while (3 | 1) ^ 2 would be 3 ^ 2 = 1. The 3 is the proof.
@print("prec {} {}\n", 1 | 2 ^ 4, 3 | 1 ^ 2);
return 0;
}
+5
View File
@@ -0,0 +1,5 @@
// ERROR:5:integer literal
unit badecode;
// SPEC §6.1: error code 는 정수 리터럴이다. 식이면 중복도 예약된 0 도 검사할 수 없다.
error E { Bad = 1 + 2, }
+6
View File
@@ -0,0 +1,6 @@
// ERROR:6:requires an explicit type
unit badgtype;
// SPEC §6.1: 전역은 타입을 적는다. 다른 유닛이 읽는 링커 심볼이라
// 초기값의 생김새에 타입을 맡기면 그쪽이 보는 것이 달라진다.
static A = 1;
+8
View File
@@ -0,0 +1,8 @@
unit okglobal;
// `const` 는 여전히 추론한다 -- SPEC §6.1 이 타입을 요구하는 것은 전역 둘뿐이다.
const A = 1;
static B: i32 = 2;
var C: i32 = 3;
error E { Bad = 1, Worse = 0x10, }
+6 -2
View File
@@ -82,6 +82,10 @@ def run_case(fec: Path, path: Path) -> tuple[bool, str]:
f"--std={ROOT / 'fec'}"],
capture_output=True, text=True, timeout=30)
output = (done.stdout + done.stderr).strip()
# Diagnostics go to the diag stream. `--dump-ast` also writes the tree to
# stdout, so a marker matched against the two together would read the tree
# and never reach the error.
diags = done.stderr.strip() or output
rejected = done.returncode != 0
if want.rejected != rejected:
@@ -96,13 +100,13 @@ def run_case(fec: Path, path: Path) -> tuple[bool, str]:
return True, ""
# The marker pins where and roughly what, so a rule can be moved or reworded
# only deliberately.
first = output.split("\n", 1)[0] if output else ""
first = diags.split("\n", 1)[0] if diags else ""
at = re.search(r":(\d+):\d+: error:", first)
if not at:
return False, f"no diagnostic to match marker\n got: {first or '(silent)'}"
if int(at.group(1)) != want.line:
return False, f"marker says line {want.line}, diagnostic is line {at.group(1)}\n {first}"
if want.text and want.text.lower() not in output.lower():
if want.text and want.text.lower() not in diags.lower():
return False, f"marker wants {want.text!r}\n {first}"
return True, ""