From 1a368dc13fa86d6a3b889f024785738c2b2bd328 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:07:45 +0900 Subject: [PATCH] =?UTF-8?q?GOAL=20P1-1..1-3:=20=EB=A6=AC=ED=84=B0=EB=9F=B4?= =?UTF-8?q?=EC=9D=B4=20=EC=9E=90=EA=B8=B0=20=ED=83=80=EC=9E=85=EC=97=90=20?= =?UTF-8?q?=EC=95=88=20=EB=A7=9E=EC=9C=BC=EB=A9=B4=20=EA=B1=B0=EB=B6=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 그리고 그 자리를 파다가 더 나쁜 것이 나왔다. store 의 폭이 목적지가 아니라 값에서 왔다. 정수 리터럴은 더 좁은 것이 요구하기 전까지 i32 이므로 let b: u8 = 200; 은 4바이트가 1바이트 자리로 가는 것으로 도착하고, 4바이트를 쓰면 프레임이 그 옆에 놓은 것을 지운다. let a: i32 = 5; let b: u8 = 300; let d: u8 = 44; a 0 / b 0 / d 44 → a 5 / b 44 / d 44 폭 넓은 지역 하나만 있으면 드러나지 않아서 여태 살아 있었다. exec/narrow.fe 가 폭이 섞인 지역을 나란히 두어 고정한다. 규칙 자체는 SPEC §3 에 넣었다: 리터럴의 타입은 문맥이 요구하는 정수 타입이고, 없으면 i32 다. 범위를 벗어나면 잘리는 것이 아니라 거부된다. 앞의 단항 - 는 리터럴의 일부로 보아 i8 = -128 은 되고 u8 = -1 은 안 된다. 같이 넣은 문장 둘: - §9 미사용 타입 파라미터는 정상이다. typed handle 이 그 모양이고 구현은 이미 그렇게 동작했다. - §7.4 --no-checks 에서 오버플로는 랩어라운드로 정의된다. 타깃이 실제로 하는 일이고 미정의로 두지 않는다. 237/237, 35/35. --- SPEC.md | 9 +- fec/src/checkstm.c | 71 +++++++++++ fec/src/lowerstm.c | 8 +- fec/tests/exec/narrow.fe | 32 +++++ fec/tests/types/badlit.fe | 10 ++ fec/tests/types/badlitng.fe | 8 ++ fec/tests/types/oklit.fe | 17 +++ handoff2.md | 244 ------------------------------------ 8 files changed, 152 insertions(+), 247 deletions(-) create mode 100644 fec/tests/exec/narrow.fe create mode 100644 fec/tests/types/badlit.fe create mode 100644 fec/tests/types/badlitng.fe create mode 100644 fec/tests/types/oklit.fe delete mode 100644 handoff2.md diff --git a/SPEC.md b/SPEC.md index 150c909..c0982be 100644 --- a/SPEC.md +++ b/SPEC.md @@ -54,7 +54,7 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 - 식별자: `[A-Za-z_][A-Za-z0-9_]*`. 대소문자 구분. - 주석: `//` 줄 끝까지, `/* */` **중첩 허용**. -- 정수 리터럴: `123`, `0xFF`, `0b1010`, `0o17`, 자릿수 구분 `1_000_000`. +- 정수 리터럴: `123`, `0xFF`, `0b1010`, `0o17`, 자릿수 구분 `1_000_000`. **타입은 문맥이 요구하는 정수 타입이고, 요구하는 것이 없으면 `i32`다.** 값이 그 타입의 범위를 벗어나면 컴파일 에러다 — `let b: u8 = 300;`은 44로 잘리는 것이 아니라 거부된다. 앞의 단항 `-`는 리터럴의 일부로 보아 `let n: i8 = -128;`은 허용하고 `let u: u8 = -1;`은 거부한다. - 문자 리터럴: `'a'`, `'\n'`, `'\x41'` → 타입 `char`. - 문자열 리터럴: `"abc"` → 타입 `str`. NUL 종료 아님. 이스케이프는 문자 리터럴과 동일. 인접 리터럴 자동 연결 없음. - 불린: `true`, `false`. 옵셔널 널: `null`. @@ -569,7 +569,7 @@ pub fn main() -> !void { 동작: `core.panic(msg: str, file: str, line: u32)` 호출 → 등록된 `sys.on_exit(fn)` 정리 함수를 역순 호출 → 메시지 출력 → `sys.exit(3)`. 사용자가 `core.set_panic_handler`로 교체 가능. 일반 panic unwind나 defer 실행은 없지만 프로세스 종료 전에 반드시 복원해야 하는 자원은 allocation 없는 고정 크기 `on_exit` registry에 등록한다. -`--no-checks` 빌드에서 제거되는 것: 경계 검사, 오버플로 검사, `.?` 검사. +`--no-checks` 빌드에서 제거되는 것: 경계 검사, 오버플로 검사, `.?` 검사. **오버플로 검사가 없을 때 `+ - * /`의 결과는 랩어라운드로 정의된다** — 타깃의 정수 연산이 그대로 하는 일이며, 미정의 동작으로 두지 않는다. 즉 `--no-checks`에서 `a + b`는 `a +% b`와 같은 값을 낸다. **절대 제거되지 않는 것:** 소유권/참조 검사, 옵셔널 타입 검사, `match` 완전성 — 전부 컴파일타임이므로. ### 7.5 comptime @@ -658,6 +658,11 @@ identity를 만들지 않는다. `comptime` type 파라미터 기반 모노모피제이션. v0.1의 user-defined generic parameter는 `type`만 지원한다. +타입 파라미터는 본문에서 사용되지 않아도 된다. `struct Handle(T) { raw: u32 }`는 +정상이며 `Handle(Node)`와 `Handle(Kind)`는 서로 다른 nominal 인스턴스다 — 파라미터가 +하는 일이 저장소를 서술하는 것이 아니라 두 인스턴스를 갈라놓는 것뿐인 경우이고, +typed handle이 정확히 그 모양이다. 미사용 파라미터에 경고를 내지 않는다. + ```fe pub struct List(T) { items: ^[]T, diff --git a/fec/src/checkstm.c b/fec/src/checkstm.c index 98f4d01..f8761ff 100644 --- a/fec/src/checkstm.c +++ b/fec/src/checkstm.c @@ -549,6 +549,62 @@ int m7_actual_compatible(FeType *want, FeType *got, FeNode *value) return compatible(want,got,value); } +/* The magnitude an integer literal spells, ignoring any sign. The same shape + lowering uses on the same text, so the two cannot disagree about what was + written. */ +static unsigned long literal_magnitude(const char *s) +{ + unsigned long v = 0; + if (!s) return 0; + if (s[0]=='0' && (s[1]=='x' || s[1]=='X')) { + for (s += 2; *s; ++s) { + int d = *s>='0'&&*s<='9' ? *s-'0' : + *s>='a'&&*s<='f' ? *s-'a'+10 : + *s>='A'&&*s<='F' ? *s-'A'+10 : -1; + if (d < 0) { if (*s=='_') continue; break; } + v = v*16UL + (unsigned long)d; + } + return v; + } + for (; *s; ++s) { + if (*s=='_') continue; + if (*s<'0' || *s>'9') break; + v = v*10UL + (unsigned long)(*s-'0'); + } + return v; +} + +/* SPEC 4.1: an integer literal takes the type its context asks for, and a + value that does not fit that type is a mistake where it is written rather + than a truncation nobody sees. */ +static int literal_fits(const FeType *want, const char *text, int negative) +{ + unsigned long v; + unsigned long limit; + unsigned bits; + if (!want || want->kind != FE_TYPE_INT || !text) return 1; + bits = want->bits ? want->bits : 32U; + if (bits > 32U) bits = 32U; + v = literal_magnitude(text); + if (want->is_unsigned) { + if (negative) return v == 0UL; + if (bits >= 32U) return 1; + return v <= (1UL << bits) - 1UL; + } + limit = bits >= 32U ? 2147483647UL : (1UL << (bits - 1U)) - 1UL; + return v <= (negative ? limit + 1UL : limit); +} + +/* Is this node a plain integer literal, rather than a character, a string, or + one of the word-shaped literals? */ +static int plain_int_literal(const FeNode *n) +{ + return n && n->kind==FE_N_LITERAL && n->text && + n->text[0]!='\'' && n->text[0]!='"' && + strcmp(n->text,"true") && strcmp(n->text,"false") && + strcmp(n->text,"null") && strcmp(n->text,"undefined"); +} + FeType *m7_check_expected(FeCheckerState *s, FeNode *value, FeType *expected) { @@ -573,6 +629,21 @@ FeType *m7_check_expected(FeCheckerState *s, FeNode *value, value->sem_context=expected; return expected; } + /* An integer literal is `i32` on its own; where an integer type is asked + for it is that type instead, and it has to fit in it. */ + if (expected && expected->kind==FE_TYPE_INT) { + FeNode *lit = plain_int_literal(value) ? value : + (value->kind==FE_N_UNARY && value->text && + !strcmp(value->text,"-") && plain_int_literal(value->a) + ? value->a : 0); + if (lit) { + if (!literal_fits(expected, lit->text, lit!=value)) + err(s->c,value->loc,"integer literal out of range for its type"); + lit->sem_type=expected; + value->sem_type=expected; + return expected; + } + } actual=check_expr(s,value); if (!expected) return actual; if (expected->kind==FE_TYPE_OPTIONAL && expected->elem && diff --git a/fec/src/lowerstm.c b/fec/src/lowerstm.c index 33665e3..78862ac 100644 --- a/fec/src/lowerstm.c +++ b/fec/src/lowerstm.c @@ -8,7 +8,13 @@ void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, fe_ir_copy(L->m, L->b, dst, value.place, size); return; } - fe_ir_store(L->m, L->b, dst, as_value(L, value, n), value.type); + /* How wide the store is belongs to the place, not to the value. An + integer literal is `i32` until something narrower asks for it, so + `let b: u8 = 200;` arrives here as four bytes going into one -- and + writing four wipes out whatever the frame put next to it. */ + fe_ir_store(L->m, L->b, dst, as_value(L, value, n), + size == 1UL ? FE_IR_I8 : + size == 2UL ? FE_IR_I16 : value.type); } void lower_return(Lower *L, FeNode *n) diff --git a/fec/tests/exec/narrow.fe b/fec/tests/exec/narrow.fe new file mode 100644 index 0000000..bf7c30d --- /dev/null +++ b/fec/tests/exec/narrow.fe @@ -0,0 +1,32 @@ +// EXIT:0 +// OUTPUT:a 5 b 200 c 44 d 9 +// OUTPUT:e 5 f 1 g 65535 +// OUTPUT:sum 253 +unit narrow; + +import std.io; + +// How wide a store is belongs to the place, not to the value. An integer +// literal is `i32` until something narrower asks for it, so `let b: u8 = 200;` +// arrives at the store as four bytes going into one -- and writing four wipes +// out whatever the frame put beside it. +// +// Several locals of mixed width, next to each other, is what it takes to see +// it: each narrow store used to reach back over the one declared before it. + +fn main() -> i32 { + let a: i32 = 5; + let b: u8 = 200; + let c: u8 = 44; + let d: i16 = 9; + @print("a {} b {} c {} d {}\n", a, b, c, d); + + let e = 5; // no annotation: i32 (SPEC 4.1) + let f: i8 = 1; + let g: u16 = 65535; + @print("e {} f {} g {}\n", e, f as i32, g); + + // And the values are still there after everything else was written. + @print("sum {}\n", (b as i32) + (c as i32) + (d as i32) + e - (a as i32)); + return 0; +} diff --git a/fec/tests/types/badlit.fe b/fec/tests/types/badlit.fe new file mode 100644 index 0000000..ede7d0a --- /dev/null +++ b/fec/tests/types/badlit.fe @@ -0,0 +1,10 @@ +// ERROR:8:integer literal out of range +unit badlit; + +// SPEC 4.1: an integer literal takes the type its context asks for. 300 is not +// a u8, and truncating it silently is the one thing that must not happen. + +fn bad() -> u8 { + let n: u8 = 300; + return n; +} diff --git a/fec/tests/types/badlitng.fe b/fec/tests/types/badlitng.fe new file mode 100644 index 0000000..b222970 --- /dev/null +++ b/fec/tests/types/badlitng.fe @@ -0,0 +1,8 @@ +// ERROR:6:integer literal out of range +unit badlitng; + +// An unsigned type has no negative values to truncate to. +fn bad() -> u8 { + let n: u8 = -1; + return n; +} diff --git a/fec/tests/types/oklit.fe b/fec/tests/types/oklit.fe new file mode 100644 index 0000000..7394afb --- /dev/null +++ b/fec/tests/types/oklit.fe @@ -0,0 +1,17 @@ +unit oklit; + +// The edges of each type are in range, and a literal with no context is i32. + +fn ok() -> i32 { + let a: u8 = 255; + let b: i8 = -128; + let c: i8 = 127; + let d: u16 = 65535; + let e: i32 = 2147483647; + let f: i32 = -2147483648; + let g: u8 = 0xFF; + let h = 5; + let i: usize = 4294967295; + return h + (a as i32) + (b as i32) + (c as i32) + (d as i32) + + (e - e) + (f - f) + (g as i32) + (i as i32); +} diff --git a/handoff2.md b/handoff2.md deleted file mode 100644 index d051e48..0000000 --- a/handoff2.md +++ /dev/null @@ -1,244 +0,0 @@ -# Handoff 2 — 마커 증거 수집과 fixture 이름 짓기 - -앞선 시도가 한 번 실패해서 되돌렸다(`7636a41`). 실패 원인부터 읽어라. -그게 이 문서의 절반이다. - -## 실패한 방식 - -지난번 에이전트는 **파일을 열지 않고 이름 문자열만 변환했다.** 밑줄을 지우고, -그 결과 충돌하는 이름에 숫자를 붙였다. - -``` -bad_loop → badlop1 badweak → badweak2 -badloop → badlp2 oktry → oktry2 -``` - -결정적 증거는 이것이다. `own/badfld.fe` 는 **구조체 필드에 참조를 둔 것**을 검사하고 -`types/badfld.fe` 는 **없는 필드에 접근한 것**을 검사한다. 완전히 다른 규칙인데 -둘 다 `badfmem.fe` 가 됐다. 같은 옛 이름에 같은 치환을 먹였기 때문이다. - -**이 일의 본질은 파일을 읽고 무엇을 검사하는지 판단하는 것이다.** 이름 변환이 아니다. - ---- - -## 배경 - -`fec` 는 Ferro 언어의 컴파일러이고 현재 **프론트엔드만** 있다. fixture 는 컴파일러가 -어떤 코드를 받아들이고 어떤 코드를 거부하는지 고정하는 테스트다. 파일 하나가 케이스 -하나다. - -```powershell -uv run python tests/run.py # 전체 -uv run python tests/run.py -k own # 경로에 own 이 들어간 것만 -``` - -러너는 매번 `.build\fec.exe` 를 새로 빌드한다. 진단 전문을 보려면 그걸 직접 부른다. - -``` -> .build\fec.exe --check fec\tests\types\bad_ari.fe -fec/tests/types/bad_ari.fe:8:15: error: wrong number of arguments - 8 | return add(1); - | ^ -``` - -### 러너가 기대를 정하는 방법 — 이걸 정확히 알아야 한다 - -`tests/run.py` 의 `expectation()` 을 직접 읽어라. 요약하면: - -| 파일 첫 줄 | 러너의 기대 | -|---|---| -| `// ERROR:7:borrow` | **거부**되고, 진단이 **7번 줄**, 문구에 **`borrow`** 포함 | -| `// ERROR:borrow` | 거부되고 문구에 `borrow` 포함 (줄은 안 봄) | -| **마커 없음** | **파일명이 `bad` 로 시작하면 거부**, 아니면 **성공** | - -마지막 줄이 이 작업의 핵심 함정이다. - -> ### 마커가 없는 파일에서는 `bad` 접두사가 기대값 그 자체다 -> -> 마커 없는 `bad_ari.fe` 를 `arity.fe` 로 바꾸면 러너는 그 순간부터 -> **성공하기를** 기대한다. 그리고 그 fixture 는 실패한다. -> -> 마커가 **있는** 파일은 마커가 기대를 정하므로 접두사가 아무 의미도 없다. -> 마음대로 지어도 된다. - -그래서 이 핸드오프는 마커 없는 37개를 **먼저** 처리한다. - ---- - -## 착수 전 기준선 - -``` -uv run python tests/run.py -→ 150/188 passed (58 pin a line and message) -``` - -**두 숫자 모두 끝까지 변하면 안 된다.** - -- 줄면 무언가 깨진 것이다 -- **늘면 검사를 약화시킨 것이다.** 이쪽이 더 나쁘다. 조용히 통과하는 테스트는 - 없는 테스트보다 해롭다 - -어느 쪽이든 되돌리고 보고하라. - ---- - -# 1. 마커 없는 fixture 37개 — 증거만 모은다 - -이 37개는 마커가 없어서 **"거부되기만 하면 통과"** 다. 엉뚱한 이유로 거부돼도 초록이다. -마커를 붙여야 하는데 **그 판정은 네가 하지 않는다.** - -``` -types/ 19 bad_ari bad_asgn bad_cast bad_cond bad_mlet bad_ret bad_shwr - bad_type bad_unit bad_unk bad_void badarr badchar badcycle - badfield badfld badindex badmat badstr - -format/ 10 bad_ari bad_bufw bad_cls bad_many bad_open bad_run bad_try - bad_type bad_verb bad_writ - -own/ 8 bad_clos bad_cond bad_dbl bad_dest bad_drop bad_loop bad_move - bad_proj -``` - -## 왜 판정을 맡기지 않는가 - -판정 결과가 세 갈래로 갈리는데 그중 하나는 **컴파일러를 고쳐야 하는 경우**다. - -| 결론 | 조치 | -|---|---| -| 마커를 안 붙였을 뿐, 진단은 옳다 | 마커를 쓴다 | -| 진단은 나오지만 **다른 이유**로 거부하고 있다 | fixture 를 다시 본다 | -| **진단이 부실하다** — 규칙 위반을 못 짚고 뭉뚱그린 오류만 낸다 | **컴파일러를 고친다** | - -세 번째가 실제로 있었다. `own/badweak.fe` 는 mut 대여를 shared 로 약화시키는 것을 -검사하는데 컴파일러는 `type mismatch` 라고만 했다. 실제 출력을 마커에 그대로 -베꼈다면 초록이 되면서 **컴파일러의 부실한 진단이 정답으로 굳었을 것이다.** - -그래서 **너는 증거를 모으고 판정은 사람이 한다.** - -## 파일마다 보고할 것 - -``` -파일 fec/tests/types/bad_ari.fe -검사 대상 이 코드가 무엇을 위반하려 하는가 — 네가 읽고 판단한 것 -근거 그렇게 본 이유. 어느 줄의 무엇 때문인지 -실제 진단 .build\fec.exe --check <경로> 의 출력 전문 (줄·열·문구 그대로) -일치 여부 실제 진단이 '검사 대상' 을 짚는가 — 예 / 아니오 / 애매 -``` - -`일치 여부` 가 이 작업의 산출물이다. 나머지는 그 판단의 근거다. -**애매하면 애매하다고 써라.** 억지로 '예' 로 만들면 이 작업이 무의미해진다. - -## 절대 금지 - -- **`// ERROR:` 마커를 하나도 쓰지 마라.** 이 항목의 산출물은 보고서뿐이다 -- `fec/src/` 의 어떤 파일도 고치지 마라 -- `tests/run.py` 를 고치지 마라 (읽는 건 권장) -- fixture 의 내용을 고치지 마라 -- **실제 출력을 그대로 마커로 옮기는 것** — 가장 하기 쉽고 가장 해로운 실수다 - -## 산출물 - -리포지터리 루트에 `fixture-report.md`. 디렉터리별로 나누고 위 다섯 항목을 담는다. -이것만 커밋한다. - ---- - -# 2. fixture 이름 짓기 - -1번을 **끝내고 보고한 뒤에** 시작한다. 1번의 판정 결과가 이름을 바꾸기 때문이다. - -대상은 네 디렉터리의 모든 `.fe` 파일이다. - -``` -fec/tests/types/ 31 -fec/tests/format/ 13 -fec/tests/own/ 50 -fec/tests/optional/ 28 -``` - -`units/` `generic/` `parse/` `pending-backend/` 는 **제외한다.** -(`units/` 와 `generic/` 은 이름이 import 경로의 일부라 구조가 다르다.) - -이미 제대로 된 이름 셋은 손대지 마라. - -``` -own/globalm.fe own/localesc.fe own/self_fld.fe -``` - -## 이름 제약 (컴파일러가 강제한다) - -각 `.fe` 는 `unit <이름>;` 을 갖고 **그 이름이 파일명(확장자 제외)과 정확히 같아야 한다.** -이름은 소문자로 시작, `a-z0-9_` 만, **최대 8자**. DOS 8.3 에서 온 제약이고 -`SPEC.md` §8.1 의 일부라 바꿀 수 없다. - -### 디렉터리가 다르면 이름이 겹쳐도 된다 - -지금 `types/bad_ari.fe` 와 `format/bad_ari.fe` 가 **동시에 존재하고 테스트는 통과한다.** -각 fixture 는 독립된 빌드다. 지난번 실패는 이걸 몰라서 억지로 유일하게 만들려다 -숫자를 붙인 것이다. **각 디렉터리 안에서만 유일하면 된다.** - -### 접두사 - -| | | -|---|---| -| **마커가 있는 파일** | `bad`/`ok` 접두사를 **버려도 된다.** 기대는 마커가 정한다. 8자를 접두사에 쓰지 마라 | -| **1번의 37개** | 사람이 마커를 붙이기 전까지 **`bad` 접두사를 반드시 유지해야 한다.** 남는 건 5자다 | - -37개는 5자 안에 뜻을 담기 어려우니 **가능한 만큼만 개선하고, 안 되는 건 그대로 두고 -목록에 적어라.** 마커가 붙으면 그때 다시 짓는다. - -## 절차 - -파일 하나마다: - -1. **연다.** 전체를 읽는다. 대개 10줄 미만이다 -2. **무엇을 검사하는지 판단한다.** 마커가 있으면 강한 단서다 -3. 그것을 8자 안에 나타내는 이름을 짓는다 -4. `git mv` 로 옮긴다 -5. **파일 안 `unit` 선언을 새 이름으로 고친다.** 안 하면 컴파일러가 거부한다 -6. 그 외에는 파일을 **한 글자도** 건드리지 마라 - -## 이름의 기준 - -이름은 **"무엇을 검사하는가"** 를 나타낸다. - -``` -좋음: badgmut → globalm 전역을 mut 로 빌리는 것 - badlocsl → localesc 지역 변수의 참조가 탈출하는 것 - badfld → reffield 구조체 필드에 참조를 둔 것 (own/) - badfld → nofield 없는 필드에 접근한 것 (types/) - -나쁨: badarr → ba1 아무것도 말하지 않음 - badweak → badweak2 숫자는 정보가 아님 - badcatch → badcatc 그냥 자른 것 -``` - -축약은 해도 된다. 다만 **읽어서 짐작이 가야 한다.** - -## 절대 금지 - -- **첫 줄 `// ERROR:` 마커를 만들거나 고치거나 지우지 마라** -- `unit` 선언 외의 내용 변경 -- 빈 줄 추가 — 지난번에 119개 파일에 군더더기 빈 줄이 들어갔다 - -## 진행 방법 - -**디렉터리 하나씩** 끝내고 `-k <디렉터리>` 로 확인한 뒤 커밋해라. 범위가 좁아야 -문제를 찾는다. - ---- - -# 커밋 - -1번은 하나, 2번은 디렉터리마다 하나. 메시지는 무엇을 왜 바꿨는지 한국어로. -**푸시하지 마라.** - -# 최종 보고 - -- 각 커밋 전후의 두 숫자 (`N/188`, `M pin`) -- 1번: `fixture-report.md` 경로, 그리고 **`일치 여부: 아니오 / 애매`** 로 판정한 것의 목록. - 거기가 컴파일러를 고쳐야 할 수도 있는 지점이라 가장 중요하다 -- 2번: **바꾼 이름 전체 목록** — `디렉터리/이전 → 이후` 와 각각 **한 줄 근거**. - 근거가 안 써지는 이름은 잘못 지은 것이다 -- 2번에서 이름을 못 지은 파일 목록과 이유. 억지로 짓지 말고 남겨라 -- 판단이 필요해서 건너뛴 것