diff --git a/.gitignore b/.gitignore index 6e14839..c996fe0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,9 @@ *.log *.lock .DS_Store + +# Go interpreter build artifact +go/yanghoscript + +# Local build / tooling output +result diff --git a/README.md b/README.md index a74b897..46a028b 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,209 @@ -# YanghoScript - Chuyển đổi từ TypeScript sang Go +# YanghoScript -Nhánh này (rewrite-in-go) được dùng để chuyển mã nguồn YanghoScript từ TypeScript sang Go. +Ngôn ngữ thử nghiệm theo **hướng hàm (functional-first)**: gán tên một lần trong scope (`CHOT`), hàm là giá trị (lambda `THE (…) ME … MAY`), literal list `[…]`, và các hàm có sẵn **PHANG** (map), **LOC** (filter), **GAP** (fold). Từ khóa viết theo kiểu **slang tiếng Việt** (chữ Latin in hoa, không dấu), kèm **alias** cho các dạng cũ / ngắn hơn. -## Mục đích +Trình thông dịch hiện tại viết bằng **Go** (nhánh `rewrite-in-go`): lexer → parser → AST → duyệt cây. Bản **TypeScript** cũ nằm trong [`ts/`](ts/). -- Viết lại toàn bộ Lexer, Token và Parser từ TypeScript sang Go. -- Đảm bảo tính tương đương về logic giữa hai phiên bản. -- Tối ưu hiệu suất và cải thiện cấu trúc. -- Loại bỏ những giới hạn của JavaScript runtime bằng việc dùng Go. +--- -## Trạng thái +## Mục lục -- Lexer: Hoàn thành. -- Token: Hoàn thành. -- Parser: Chưa bắt đầu. +1. [Ý tưởng ngôn ngữ](#1-ý-tưởng-ngôn-ngữ) +2. [Từ vựng và từ khóa](#2-từ-vựng-và-từ-khóa) +3. [Cú pháp](#3-cú-pháp) +4. [Hàm dựng sẵn cho list](#4-hàm-dựng-sẵn-cho-list) +5. [Phần chưa hỗ trợ](#5-phần-chưa-hỗ-trợ) +6. [Chạy chương trình (CLI)](#6-chạy-chương-trình-cli) +7. [Cấu trúc kho mã](#7-cấu-trúc-kho-mã) +8. [File ví dụ](#8-file-ví-dụ) +9. [Giấy phép](#9-giấy-phép) -## Cách sử dụng +--- -1. Clone repo: - ```sh - git clone -b rewrite-in-go https://github.com/hoach-linux/yanghoscript.git - ``` -2. Chạy lexer: - ```sh - go run main.go - ``` -3. Đóng góp: PRs hoặc issue luôn được chào đón! +## 1. Ý tưởng ngôn ngữ + +- **Gán chỉ qua `CHOT`** — không dùng `=` đứng một mình làm phép gán; parser thiết kế theo một kiểu viết thống nhất. +- **Bất biến trong scope:** gán lại `CHOT` cùng tên trong cùng scope sẽ lỗi khi chạy. +- **Hàm** khai báo bằng `THE tên(tham_số) ME … MAY` hoặc lambda `THE (a, b) ME … MAY`; trả về bằng **`TRA` biểu_thức**. +- **Điều kiện:** `NEU (điều_kiện) ME … MAY` và nhánh còn lại **`THOI ME … MAY`** (hoặc `KOTHI` thay cho `THOI`). +- **Hiệu ứng phụ in ra** — **`NOILIENTUC biểu_thức IM`** (alias `KEU`). +- **Danh sách** — literal `[a, b, c]` và hàm bậc cao `PHANG`, `LOC`, `GAP`. + +--- + +## 2. Từ vựng và từ khóa + +Định danh: `[a-zA-Z_][a-zA-Z0-9_]*`. Số nguyên (`\d+`). Chuỗi trong **dấu nháy đơn** `'nội dung'` (một dòng tới dấu `'` tiếp theo, không escape phức tạp). + +Chú thích: `// …` và `/* … */`. + +| Vai trò | Dạng chính | Alias (cùng nghĩa) | +|---------|------------|---------------------| +| Kết thúc câu lệnh | `IM` | `DI` | +| Khối | `ME … MAY` | `MO … DONG`, hoặc `{ … }` | +| Hàm / lambda | `THE` | `HUA` | +| In (log) | `NOILIENTUC` | `KEU` | +| Trả về | `TRA` | — | +| Nếu | `NEU` | — | +| Thì / ngược lại | `THOI` | `KOTHI` | +| Gán một lần | `CHOT` | — | +| Bằng | `UYTIN` hoặc `==` | | +| Khác | `KHONGUYTIN` hoặc `!=` | | +| Lớn / nhỏ | `NHIEUHON` / `ITHON` hoặc `>` / `<` | | +| ≥ / ≤ | `NHIEUHONHOACUYTIN` / `ITHONHOACUYTIN` hoặc `>=` / `<=` | | + +Vòng lặp (`VONG`, `CHO`, …) lexer vẫn nhận diện nhưng **parser chưa xử lý** — không dùng được trong chương trình. + +--- + +## 3. Cú pháp + +### Gán và biểu thức + +```text +CHOT x = 10 IM +CHOT s = 'hello' IM +NOILIENTUC x + 1 IM +``` + +Phép tính: `+`, `-`, `*`, `/`, ngoặc. So sánh như bảng trên. + +### Khối và hàm + +```text +THE add(a, b) ME + TRA a + b IM +MAY + +CHOT f = THE (x) ME TRA x * 2 IM MAY IM +NOILIENTUC add(3, 4) IM +NOILIENTUC f(5) IM +``` + +Hàm có tên: `THE tên(…) ME thân MAY`. Lambda: `THE (tham_số) ME … MAY`. Lambda có thể là giá trị (`CHOT triple = THE(x) ME TRA x * 3 IM MAY IM`) và gọi `triple(7)`. + +### Điều kiện + +```text +NEU (x UYTIN 0) ME + NOILIENTUC 'positive' IM +MAY +THOI ME + NOILIENTUC 'non-positive' IM +MAY +``` + +Có thể lồng `NEU` trong nhánh `THOI` (xem [`examples/code.ys`](examples/code.ys)). + +### List + +```text +CHOT xs = [1, 2, 3] IM +``` + +Phần tử là biểu thức, cách nhau bằng dấu phẩy. + +--- + +## 4. Hàm dựng sẵn cho list + +Cả ba đều là hàm **native** với số tham số cố định; tham số thứ hai (với `GAP` là thứ ba) phải là **lambda**, không phải hàm built-in cùng tên. + +| Tên | Ý nghĩa | Cách gọi | +|-----|---------|----------| +| **PHANG** | map | `PHANG(list, THE(x) ME … MAY)` — hàm **một** tham số | +| **LOC** | filter | `LOC(list, THE(x) ME … MAY)` — vị ngữ một tham số | +| **GAP** | fold | `GAP(list, giá_trị_ban_đầu, THE(acc, x) ME … MAY)` — lambda hai tham số | + +Ví dụ: + +```text +CHOT xs = [1, 2, 3] IM +CHOT doubled = PHANG(xs, THE(n) ME TRA n * 2 IM MAY) IM +CHOT evens = LOC(xs, THE(n) ME TRA n UYTIN 2 IM MAY) IM +CHOT sum = GAP(xs, 0, THE(a, b) ME TRA a + b IM MAY) IM +``` + +--- + +## 5. Phần chưa hỗ trợ + +- Vòng lặp (`VONG`, `CHO`, …) — mới là token, chưa có AST. +- Import module, kiểu tĩnh, class, ngoại lệ kiểu truyền thống — ngoài phạm vi trình thông dịch hiện tại. + +--- + +## 6. Chạy chương trình (CLI) + +Cần dùng trình thông dịch **Go** trong thư mục **`go/`** của nhánh này. Gói **npm** `yanghoscript` cũ **không** hiểu `CHOT`, `THE`, list, `PHANG`, v.v.; lỗi **`Error at this position …`** ở token “mới” đầu tiên thường do đang chạy **sai** binary (không phải bản build từ repo). + +Build và chạy: + +```bash +cd go +CGO_ENABLED=0 go build -o yanghoscript ./cmd/yanghoscript +./yanghoscript run đường/dẫn/file.ys +# tương đương: +./yanghoscript đường/dẫn/file.ys +``` + +**Quan trọng:** sau `go build`, file chạy là **`./yanghoscript`**. Lệnh **`yanghoscript`** không có **`./`** lấy binary trên **PATH** — thường là bản npm cũ → lại lỗi `Error at this position`. + +Kiểm tra phiên bản: + +```bash +./yanghoscript --version +``` + +Kết quả mong đợi dạng **`0.2.0-go`** (xem `go/internal/version`). Nếu gọi `yanghoscript` không `./` mà số phiên bản khác — đó là binary khác. + +Chạy không phụ thuộc PATH (từ thư mục gốc repo): + +```bash +./scripts/run-yanghoscript examples/code.ys +``` + +Máy không có `gcc`, hãy dùng **`CGO_ENABLED=0`** như trên. + +--- + +## 7. Cấu trúc kho mã + +| Đường dẫn | Vai trò | +|-----------|---------| +| `go/cmd/yanghoscript` | Điểm vào CLI | +| `go/internal/cli` | Cobra, đọc `.ys` | +| `go/internal/lexer` | Token / từ khóa | +| `go/internal/parser` | Parse ra AST | +| `go/internal/ast` | Cây cú pháp | +| `go/internal/interpreter` | Thực thi + `PHANG` / `LOC` / `GAP` | +| `go/internal/version` | Chuỗi phiên bản | + +Module: `github.com/hoachnt/yanghoscript` (`go/go.mod`). Cần **Go 1.23+**. + +--- + +## 8. File ví dụ + +| File | Nội dung | +|------|----------| +| [`examples/code.ys`](examples/code.ys) | Tổng quan: `CHOT`, so sánh, `NEU`/`THOI`, đệ quy, list, lambda là giá trị, cuối file có khối **alias cũ** (`HUA`/`KEU`/…) | +| [`examples/factorial.ys`](examples/factorial.ys), [`examples/fibonacci.ys`](examples/fibonacci.ys) | Đệ quy | +| [`go/input.ys`](go/input.ys) | Script ngắn để chạy thử tay | + +Đoạn tối thiểu một phong cách: + +```text +CHOT x = 3 IM +THE double(n) ME TRA n * 2 MAY +NOILIENTUC double(x) IM + +CHOT xs = [1, 2, 3] IM +NOILIENTUC PHANG(xs, THE(a) ME TRA a + 1 IM MAY) IM +``` + +--- + +## 9. Giấy phép + +Xem file [`LICENSE`](LICENSE) ở thư mục gốc repository. diff --git a/examples/code.ys b/examples/code.ys index 04f983c..6592c7e 100644 --- a/examples/code.ys +++ b/examples/code.ys @@ -1,37 +1,49 @@ -text = 'Hoach' IM -summ = 6 + 5 IM - -NOILIENTUC text IM -NOILIENTUC summ IM - -sumandmin = summ - 20 + 2 * 2 IM -NOILIENTUC 8 + 2 * 10 IM // Must be 28, but I get 100 - -NOILIENTUC sumandmin IM +// YanghoScript showcase - functional style + Vietnamese slang (Go interpreter only) +// From repo root: ./scripts/run-yanghoscript examples/code.ys +// Or from go/: CGO_ENABLED=0 go build -o yanghoscript ./cmd/yanghoscript && ./yanghoscript --version +// (must print 0.2.0-go). If "Error at this position" - wrong binary on PATH; use script or ./yanghoscript from go/ + +// ----- Immutable bind (one assignment per scope) ----- +CHOT ten = 'Hoach' IM +CHOT tong = 6 + 5 IM +NOILIENTUC ten IM +NOILIENTUC tong IM + +// Arithmetic and operator precedence +CHOT bieu_thuc = 8 + 2 * 10 IM +NOILIENTUC bieu_thuc IM + +CHOT tru_cong = tong - 20 + 2 * 2 IM +NOILIENTUC tru_cong IM NOILIENTUC 'Chao ca lo nha minh nha' IM +// ----- Comparisons: Vietnamese keywords + ASCII operators ----- NOILIENTUC 1 UYTIN 1 IM NOILIENTUC 2 NHIEUHON 1 IM NOILIENTUC 1 ITHON 2 IM -NOILIENTUC 1 NHIEUBANG 1 IM -NOILIENTUC 2 ITBANG 2 IM - -NOILIENTUC 2 UYTIN 1 IM -NOILIENTUC 2 NHIEUHON 3 IM -NOILIENTUC 1 ITHON 0 IM -NOILIENTUC 1 NHIEUBANG 2 IM -NOILIENTUC 2 ITBANG 1 IM - +NOILIENTUC 2 NHIEUHONHOACUYTIN 2 IM +NOILIENTUC 2 ITHONHOACUYTIN 2 IM +NOILIENTUC 2 KHONGUYTIN 3 IM + +NOILIENTUC 1 == 1 IM +NOILIENTUC 2 != 3 IM +NOILIENTUC 2 > 1 IM +NOILIENTUC 1 < 2 IM +NOILIENTUC 2 >= 2 IM +NOILIENTUC 1 <= 2 IM + +// ----- Conditionals: close then-branch before else-branch ----- NEU (2 UYTIN 1) ME NOILIENTUC 'Yasuo' IM -MAY KOTHI NEU (2 NHIEUHON 1) ME +MAY +THOI NEU (2 NHIEUHON 1) ME NOILIENTUC 'Kosuo' IM -MAY KOTHI ME +MAY +THOI ME NOILIENTUC 'Default' IM MAY - -// Create a function +// ----- Named functions, return, calls ----- THE greet(name) ME NOILIENTUC 'Hello, ' + name IM MAY @@ -40,28 +52,45 @@ THE cong(a, b) ME TRA a + b IM MAY - -// Call a function greet('Hoachnt') IM NOILIENTUC cong(1, 2) IM - - -number = 5 IM +// ----- Recursion ----- +CHOT number = 5 IM THE recursion(n) ME NEU (n ITHON 1) ME TRA 1 IM - MAY KOTHI ME + MAY + THOI ME NOILIENTUC n IM - TRA recursion(n - 1) IM - MAY + MAY MAY - recursion(number) IM -NOILIENTUC 'All Works!!!' IM +// ----- Lists: map / filter / fold builtins ----- +CHOT xs = [1, 2, 3, 4, 5] IM + +CHOT gap_doi = PHANG(xs, THE(x) ME TRA x * 2 IM MAY) IM +NOILIENTUC gap_doi IM + +CHOT lon_hon = LOC(xs, THE(x) ME TRA x NHIEUHON 2 IM MAY) IM +NOILIENTUC lon_hon IM + +CHOT tong_list = GAP(xs, 0, THE(a, b) ME TRA a + b IM MAY) IM +NOILIENTUC tong_list IM + +// ----- First-class lambda value ----- +CHOT triple = THE(x) ME TRA x * 3 IM MAY IM +NOILIENTUC triple(7) IM + +// ----- Legacy keyword aliases (still supported) ----- +HUA legacy() MO + KEU 'Legacy alias OK' DI +DONG + +legacy() DI -// NOILIENTUC 'Hello world' - comment \ No newline at end of file +NOILIENTUC 'All features demo done!' IM diff --git a/go/input.ys b/go/input.ys index 42a8084..b69f96a 100644 --- a/go/input.ys +++ b/go/input.ys @@ -1,34 +1,43 @@ -// Basic example +// YanghoScript: functional style + Vietnamese slang keywords -a = 3 IM -b = 4 IM +CHOT a = 3 IM +CHOT b = 4 IM NEU (2 ITHON 1) ME NOILIENTUC 'Yasuo' IM -MAY KOTHI ME +MAY +THOI ME NOILIENTUC 'KO SUO' IM MAY NOILIENTUC (a + b) * 3 IM NOILIENTUC 'Hehe boi' IM - -number = 5 IM +CHOT number = 5 IM THE recursion(n) ME NEU (n ITHON 1) ME TRA 1 IM - MAY KOTHI ME + MAY + THOI ME NOILIENTUC n IM - TRA recursion(n - 1) IM - MAY + MAY MAY THE greet(name) ME TRA name IM MAY - recursion(5) IM -NOILIENTUC greet('Hoach') IM \ No newline at end of file +NOILIENTUC greet('Hoach') IM + +CHOT xs = [1, 2, 3] IM +CHOT doubled = PHANG(xs, THE(x) ME TRA x * 2 IM MAY) IM +NOILIENTUC doubled IM + +CHOT big = LOC(xs, THE(x) ME TRA x NHIEUHON 1 IM MAY) IM +NOILIENTUC big IM + +CHOT sum = GAP(xs, 0, THE(a, b) ME TRA a + b IM MAY) IM +NOILIENTUC sum IM diff --git a/go/internal/ast/ast.go b/go/internal/ast/ast.go index d66c188..79eed11 100644 --- a/go/internal/ast/ast.go +++ b/go/internal/ast/ast.go @@ -1,16 +1,13 @@ package ast -// Node - базовый интерфейс для всех узлов AST type Node interface { Accept(visitor Visitor) any } -// ExpressionNode - базовый интерфейс для всех выражений type ExpressionNode interface { Node } -// StatementsNode - список выражений (инструкций) type StatementsNode struct { Statements []Node } @@ -19,7 +16,6 @@ func (s *StatementsNode) Accept(visitor Visitor) any { return visitor.VisitStatementsNode(s) } -// NumberNode - числовой литерал type NumberNode struct { Value float64 } @@ -28,7 +24,6 @@ func (n *NumberNode) Accept(visitor Visitor) any { return visitor.VisitNumberNode(n) } -// StringNode - строковый литерал type StringNode struct { Value string } @@ -37,7 +32,14 @@ func (s *StringNode) Accept(visitor Visitor) any { return visitor.VisitStringNode(s) } -// VariableNode - переменная +type ListNode struct { + Elements []ExpressionNode +} + +func (l *ListNode) Accept(visitor Visitor) any { + return visitor.VisitListNode(l) +} + type VariableNode struct { Name string } @@ -46,17 +48,17 @@ func (v *VariableNode) Accept(visitor Visitor) any { return visitor.VisitVariableNode(v) } -// AssignNode - присваивание +// AssignNode binds a name. Immutable=true means CHOT (single assignment per scope). type AssignNode struct { - Variable *VariableNode - Value ExpressionNode + Variable *VariableNode + Value ExpressionNode + Immutable bool } func (a *AssignNode) Accept(visitor Visitor) any { return visitor.VisitAssignNode(a) } -// BinOperationNode - бинарная операция type BinOperationNode struct { Left ExpressionNode Operator string @@ -67,7 +69,6 @@ func (b *BinOperationNode) Accept(visitor Visitor) any { return visitor.VisitBinOperationNode(b) } -// UnarOperationNode - унарная операция type UnarOperationNode struct { Operator string Operand ExpressionNode @@ -77,7 +78,6 @@ func (u *UnarOperationNode) Accept(visitor Visitor) any { return visitor.VisitUnarOperationNode(u) } -// IfNode - условный оператор type IfNode struct { Condition ExpressionNode ThenBranch Node @@ -88,7 +88,6 @@ func (i *IfNode) Accept(visitor Visitor) any { return visitor.VisitIfNode(i) } -// ReturnNode - оператор возврата type ReturnNode struct { Value ExpressionNode } @@ -97,20 +96,22 @@ func (r *ReturnNode) Accept(visitor Visitor) any { return visitor.VisitReturnNode(r) } -// FunctionDeclarationNode - объявление функции +// FunctionDeclarationNode: Name empty = lambda expression. Native is built-in (PHANG, LOC, GAP). type FunctionDeclarationNode struct { Name string Parameters []string Body *StatementsNode + Native string } func (f *FunctionDeclarationNode) Accept(visitor Visitor) any { return visitor.VisitFunctionDeclarationNode(f) } -// FunctionCallNode - вызов функции +// FunctionCallNode: use Name for foo(...); Callee for higher-order (expr)(...). type FunctionCallNode struct { Name string + Callee ExpressionNode Arguments []ExpressionNode } @@ -118,11 +119,11 @@ func (f *FunctionCallNode) Accept(visitor Visitor) any { return visitor.VisitFunctionCallNode(f) } -// Visitor - интерфейс посетителя для обхода AST type Visitor interface { VisitStatementsNode(node *StatementsNode) any VisitNumberNode(node *NumberNode) any VisitStringNode(node *StringNode) any + VisitListNode(node *ListNode) any VisitVariableNode(node *VariableNode) any VisitAssignNode(node *AssignNode) any VisitBinOperationNode(node *BinOperationNode) any diff --git a/go/internal/cli/root.go b/go/internal/cli/root.go index 5241f85..fcd8bcb 100644 --- a/go/internal/cli/root.go +++ b/go/internal/cli/root.go @@ -1,18 +1,36 @@ package cli import ( + "fmt" + "strings" + "github.com/spf13/cobra" + + "github.com/hoachnt/yanghoscript/internal/version" ) // NewRootCommand создает корневую команду CLI func NewRootCommand() *cobra.Command { rootCmd := &cobra.Command{ - Use: "yanghoscript", + Use: "yanghoscript [file.ys]", Short: "YanghoScript CLI", - Long: "YanghoScript CLI for running and managing .ys scripts", + Long: fmt.Sprintf(`YanghoScript CLI - run .ys files with the Go interpreter (CHOT, THE, PHANG, lists, ...). + +Same as: yanghoscript run +Use --version: must show %s (rewrite-in-go). If not, you are running another binary from PATH.`, version.Version), + Args: cobra.ArbitraryArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return cmd.Help() + } + if len(args) == 1 && strings.HasSuffix(args[0], ".ys") { + return RunYSFromPath(args[0]) + } + return fmt.Errorf("usage: %s run or %s ", cmd.Name(), cmd.Name()) + }, } - // Добавляем команды + rootCmd.Version = version.Version rootCmd.AddCommand(NewRunCommand()) return rootCmd diff --git a/go/internal/cli/run.go b/go/internal/cli/run.go index 725cbc5..3bd5511 100644 --- a/go/internal/cli/run.go +++ b/go/internal/cli/run.go @@ -11,6 +11,15 @@ import ( "github.com/hoachnt/yanghoscript/internal/parser" ) +// RunYSFromPath reads a .ys file and executes it (used by `run` and by `yanghoscript file.ys`). +func RunYSFromPath(path string) error { + content, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("read file: %w", err) + } + return processFile(string(content)) +} + // NewRunCommand создает команду для выполнения .ys файлов func NewRunCommand() *cobra.Command { cmd := &cobra.Command{ @@ -19,25 +28,31 @@ func NewRunCommand() *cobra.Command { Args: cobra.ExactArgs(1), SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - content, err := os.ReadFile(args[0]) - if err != nil { - return fmt.Errorf("read file: %w", err) - } - return processFile(string(content)) + return RunYSFromPath(args[0]) }, } return cmd } +func illegalByteOffset(t lexer.Token) int { + if t.Literal == "" { + return t.Pos + } + return t.Pos - len(t.Literal) +} + // processFile обрабатывает содержимое файла func processFile(content string) error { - // Создаем лексер l := lexer.NewLexer(content) - - // Генерируем токены tokens := l.Tokenize() - // Создаем парсер + for _, t := range tokens { + if t.Type == lexer.ILLEGAL { + off := illegalByteOffset(t) + return fmt.Errorf("illegal character %q at byte %d — if you see this with valid Go syntax, you may be using an old npm interpreter; build from go/ (see README)", t.Literal, off) + } + } + p := parser.New(tokens) // Парсим код diff --git a/go/internal/interpreter/interpreter.go b/go/internal/interpreter/interpreter.go index ae8ee00..f702643 100644 --- a/go/internal/interpreter/interpreter.go +++ b/go/internal/interpreter/interpreter.go @@ -7,18 +7,15 @@ import ( "github.com/hoachnt/yanghoscript/internal/lexer" ) -// Environment представляет окружение с переменными и функциями type Environment struct { Scopes []map[string]any Functions map[string]*ast.FunctionDeclarationNode } -// PushScope добавляет новую область видимости func (e *Environment) PushScope() { e.Scopes = append(e.Scopes, make(map[string]any)) } -// PopScope удаляет текущую область видимости func (e *Environment) PopScope() { if len(e.Scopes) == 0 { panic("No scope to pop") @@ -26,7 +23,6 @@ func (e *Environment) PopScope() { e.Scopes = e.Scopes[:len(e.Scopes)-1] } -// CurrentScope возвращает текущую область видимости func (e *Environment) CurrentScope() map[string]any { if len(e.Scopes) == 0 { panic("No scope available") @@ -34,7 +30,6 @@ func (e *Environment) CurrentScope() map[string]any { return e.Scopes[len(e.Scopes)-1] } -// GetVariable ищет переменную в цепочке областей видимости func (e *Environment) GetVariable(name string) (any, bool) { for i := len(e.Scopes) - 1; i >= 0; i-- { if value, exists := e.Scopes[i][name]; exists { @@ -44,22 +39,41 @@ func (e *Environment) GetVariable(name string) (any, bool) { return nil, false } -// Interpreter выполняет узлы AST type Interpreter struct { env Environment } -// NewInterpreter создает новый экземпляр интерпретатора func NewInterpreter() *Interpreter { - return &Interpreter{ + i := &Interpreter{ env: Environment{ Scopes: []map[string]any{make(map[string]any)}, Functions: make(map[string]*ast.FunctionDeclarationNode), }, } + seedNatives(i) + return i +} + +func seedNatives(i *Interpreter) { + natives := []struct { + name string + nArg int + }{ + {"PHANG", 2}, + {"LOC", 2}, + {"GAP", 3}, + } + for _, nat := range natives { + params := make([]string, nat.nArg) + for j := range params { + params[j] = "_" + } + fn := &ast.FunctionDeclarationNode{Name: nat.name, Native: nat.name, Parameters: params} + i.env.Functions[nat.name] = fn + i.env.CurrentScope()[nat.name] = fn + } } -// Run выполняет AST и возвращает результат func (i *Interpreter) Run(node ast.Node) any { switch n := node.(type) { case *ast.StatementsNode: @@ -76,6 +90,8 @@ func (i *Interpreter) Run(node ast.Node) any { return i.executeVariable(n) case *ast.StringNode: return n.Value + case *ast.ListNode: + return i.executeList(n) case *ast.FunctionDeclarationNode: return i.executeFunctionDeclaration(n) case *ast.FunctionCallNode: @@ -83,13 +99,20 @@ func (i *Interpreter) Run(node ast.Node) any { case *ast.IfNode: return i.executeIf(n) case *ast.ReturnNode: - panic(n.Value) // Используем panic для возврата значения + panic(n.Value) default: panic(fmt.Sprintf("Unknown node type: %T", n)) } } -// executeStatements выполняет список инструкций +func (i *Interpreter) executeList(node *ast.ListNode) any { + out := make([]any, 0, len(node.Elements)) + for _, el := range node.Elements { + out = append(out, i.Run(el)) + } + return out +} + func (i *Interpreter) executeStatements(node *ast.StatementsNode) any { var result any for _, stmt := range node.Statements { @@ -98,28 +121,46 @@ func (i *Interpreter) executeStatements(node *ast.StatementsNode) any { return result } -// executeAssign выполняет присваивание func (i *Interpreter) executeAssign(node *ast.AssignNode) any { + if node.Immutable { + if _, exists := i.env.CurrentScope()[node.Variable.Name]; exists { + panic("CHOT: biến đã được gán trong scope này (immutable)") + } + } value := i.Run(node.Value) i.env.CurrentScope()[node.Variable.Name] = value return value } -// executeBinOperation выполняет бинарную операцию +func cmpFloat(op string, a, b float64) bool { + switch op { + case "UYTIN", "==": + return a == b + case "NHIEUHON", ">": + return a > b + case "ITHON", "<": + return a < b + case "NHIEUHONHOACUYTIN", ">=": + return a >= b + case "ITHONHOACUYTIN", "<=": + return a <= b + case "KHONGUYTIN", "!=": + return a != b + default: + panic(fmt.Sprintf("Unknown comparison operator: %s", op)) + } +} + func (i *Interpreter) executeBinOperation(node *ast.BinOperationNode) any { left := i.Run(node.Left) right := i.Run(node.Right) - // Проверяем типы операндов switch leftTyped := left.(type) { case float64: - // Если левый операнд — число, проверяем правый операнд rightTyped, ok := right.(float64) if !ok { - panic(fmt.Sprintf("Type mismatch: cannot apply operator '%s' to types %T and %T", node.Operator, left, right)) + panic(fmt.Sprintf("Type mismatch: %T and %T", left, right)) } - - // Выполняем операцию для чисел switch node.Operator { case "+": return leftTyped + rightTyped @@ -132,33 +173,26 @@ func (i *Interpreter) executeBinOperation(node *ast.BinOperationNode) any { panic("Division by zero") } return leftTyped / rightTyped - case "UYTIN": // Equal (==) - return leftTyped == rightTyped - case "NHIEUHON": // Greater than (>) - return leftTyped > rightTyped - case "ITHON": // Less than (<) - return leftTyped < rightTyped - case "NHIEUHONHOACUYTIN": // Greater than or equal (>=) - return leftTyped >= rightTyped - case "ITHONHOACUYTIN": // Less than or equal (<=) - return leftTyped <= rightTyped - case "KHONGUYTIN": // Not equal (!=) - return leftTyped != rightTyped + case "UYTIN", "NHIEUHON", "ITHON", "NHIEUHONHOACUYTIN", "ITHONHOACUYTIN", "KHONGUYTIN", + "==", "!=", "<", ">", "<=", ">=": + return cmpFloat(node.Operator, leftTyped, rightTyped) default: panic(fmt.Sprintf("Unknown binary operator: %s", node.Operator)) } case string: - // Если левый операнд — строка, проверяем правый операнд rightTyped, ok := right.(string) if !ok { - panic(fmt.Sprintf("Type mismatch: cannot apply operator '%s' to types %T and %T", node.Operator, left, right)) + panic(fmt.Sprintf("Type mismatch: %T and %T", left, right)) } - - // Поддерживаем только конкатенацию для строк - if node.Operator == "+" { + switch node.Operator { + case "+": return leftTyped + rightTyped - } else { + case "UYTIN", "==": + return leftTyped == rightTyped + case "KHONGUYTIN", "!=": + return leftTyped != rightTyped + default: panic(fmt.Sprintf("Unsupported operator '%s' for strings", node.Operator)) } @@ -167,7 +201,6 @@ func (i *Interpreter) executeBinOperation(node *ast.BinOperationNode) any { } } -// executeUnarOperation выполняет унарную операцию func (i *Interpreter) executeUnarOperation(node *ast.UnarOperationNode) any { value := i.Run(node.Operand) if node.Operator == string(lexer.LOG) { @@ -177,7 +210,6 @@ func (i *Interpreter) executeUnarOperation(node *ast.UnarOperationNode) any { panic(fmt.Sprintf("Unknown unary operator: %s", node.Operator)) } -// executeVariable возвращает значение переменной func (i *Interpreter) executeVariable(node *ast.VariableNode) any { value, exists := i.env.GetVariable(node.Name) if !exists { @@ -186,28 +218,57 @@ func (i *Interpreter) executeVariable(node *ast.VariableNode) any { return value } -// executeFunctionDeclaration сохраняет функцию в окружении func (i *Interpreter) executeFunctionDeclaration(node *ast.FunctionDeclarationNode) any { - i.env.Functions[node.Name] = node - return nil + if node.Native != "" { + return nil + } + if node.Name != "" { + i.env.Functions[node.Name] = node + i.env.CurrentScope()[node.Name] = node + return nil + } + return node } -// executeFunctionCall runs a function call. result is named so the defer can set the return -// value after recover from TRA (implemented via panic); otherwise the caller receives nil. func (i *Interpreter) executeFunctionCall(node *ast.FunctionCallNode) (result any) { - function, exists := i.env.Functions[node.Name] - if !exists { + var fn *ast.FunctionDeclarationNode + if node.Callee != nil { + v := i.Run(node.Callee) + var ok bool + fn, ok = v.(*ast.FunctionDeclarationNode) + if !ok || fn == nil { + panic("call: callee is not a function") + } + } else { + if v, ok := i.env.GetVariable(node.Name); ok { + if f, ok := v.(*ast.FunctionDeclarationNode); ok { + fn = f + } + } + if fn == nil { + fn = i.env.Functions[node.Name] + } + } + if fn == nil { panic(fmt.Sprintf("Function '%s' not found", node.Name)) } + if fn.Native != "" { + return i.nativeCall(fn.Native, node.Arguments) + } + i.env.PushScope() defer i.env.PopScope() - for idx, param := range function.Parameters { + for idx, param := range fn.Parameters { argValue := i.Run(node.Arguments[idx]) - switch argValue.(type) { - case float64, string: - i.env.CurrentScope()[param] = argValue + switch v := argValue.(type) { + case float64, string, bool: + i.env.CurrentScope()[param] = v + case []any: + i.env.CurrentScope()[param] = v + case *ast.FunctionDeclarationNode: + i.env.CurrentScope()[param] = v default: panic(fmt.Sprintf("Unsupported argument type for parameter '%s': %T", param, argValue)) } @@ -223,11 +284,133 @@ func (i *Interpreter) executeFunctionCall(node *ast.FunctionCallNode) (result an } }() - i.Run(function.Body) + i.Run(fn.Body) + return +} + +func (i *Interpreter) nativeCall(kind string, args []ast.ExpressionNode) any { + switch kind { + case "PHANG": + if len(args) != 2 { + panic("PHANG: cần 2 đối số (list, hàm)") + } + return i.nativeMap(args) + case "LOC": + if len(args) != 2 { + panic("LOC: cần 2 đối số (list, hàm)") + } + return i.nativeFilter(args) + case "GAP": + if len(args) != 3 { + panic("GAP: cần 3 đối số (list, acc, hàm 2 tham số)") + } + return i.nativeFold(args) + default: + panic("unknown native: " + kind) + } +} + +func (i *Interpreter) nativeMap(args []ast.ExpressionNode) any { + listVal := i.Run(args[0]) + fnVal := i.Run(args[1]) + list, ok := listVal.([]any) + if !ok { + panic("PHANG: đối số 1 phải là list") + } + fn, ok := fnVal.(*ast.FunctionDeclarationNode) + if !ok || fn.Native != "" { + panic("PHANG: đối số 2 phải là hàm (lambda)") + } + if len(fn.Parameters) != 1 { + panic("PHANG: hàm cần đúng 1 tham số") + } + out := make([]any, 0, len(list)) + for _, e := range list { + out = append(out, i.callUnaryFunc(fn, e)) + } + return out +} + +func (i *Interpreter) nativeFilter(args []ast.ExpressionNode) any { + listVal := i.Run(args[0]) + fnVal := i.Run(args[1]) + list, ok := listVal.([]any) + if !ok { + panic("LOC: đối số 1 phải là list") + } + fn, ok := fnVal.(*ast.FunctionDeclarationNode) + if !ok || fn.Native != "" { + panic("LOC: đối số 2 phải là hàm (lambda)") + } + if len(fn.Parameters) != 1 { + panic("LOC: hàm cần đúng 1 tham số") + } + out := make([]any, 0) + for _, e := range list { + v := i.callUnaryFunc(fn, e) + if b, ok := v.(bool); ok && b { + out = append(out, e) + } + } + return out +} + +func (i *Interpreter) nativeFold(args []ast.ExpressionNode) any { + listVal := i.Run(args[0]) + acc := i.Run(args[1]) + fnVal := i.Run(args[2]) + list, ok := listVal.([]any) + if !ok { + panic("GAP: đối số 1 phải là list") + } + fn, ok := fnVal.(*ast.FunctionDeclarationNode) + if !ok || fn.Native != "" { + panic("GAP: đối số 3 phải là hàm (lambda)") + } + if len(fn.Parameters) != 2 { + panic("GAP: hàm cần đúng 2 tham số (acc, phần tử)") + } + for _, e := range list { + acc = i.callBinaryFunc(fn, acc, e) + } + return acc +} + +func (i *Interpreter) callUnaryFunc(fn *ast.FunctionDeclarationNode, arg any) (result any) { + i.env.PushScope() + defer i.env.PopScope() + i.env.CurrentScope()[fn.Parameters[0]] = arg + defer func() { + if r := recover(); r != nil { + if rv, ok := r.(ast.ExpressionNode); ok { + result = i.Run(rv) + } else { + panic(r) + } + } + }() + i.Run(fn.Body) + return +} + +func (i *Interpreter) callBinaryFunc(fn *ast.FunctionDeclarationNode, acc, item any) (result any) { + i.env.PushScope() + defer i.env.PopScope() + i.env.CurrentScope()[fn.Parameters[0]] = acc + i.env.CurrentScope()[fn.Parameters[1]] = item + defer func() { + if r := recover(); r != nil { + if rv, ok := r.(ast.ExpressionNode); ok { + result = i.Run(rv) + } else { + panic(r) + } + } + }() + i.Run(fn.Body) return } -// executeIf выполняет условный оператор func (i *Interpreter) executeIf(node *ast.IfNode) any { condition := i.Run(node.Condition).(bool) if condition { @@ -237,4 +420,3 @@ func (i *Interpreter) executeIf(node *ast.IfNode) any { } return nil } - diff --git a/go/internal/interpreter/interpreter_test.go b/go/internal/interpreter/interpreter_test.go index 39aa28b..e5d9dc8 100644 --- a/go/internal/interpreter/interpreter_test.go +++ b/go/internal/interpreter/interpreter_test.go @@ -1,6 +1,7 @@ package interpreter import ( + "strings" "testing" "github.com/hoachnt/yanghoscript/internal/ast" @@ -32,3 +33,24 @@ MAY t.Fatalf("greet('Hoach') = %v (%T), want Hoach", out, out) } } + +func TestCHOTDoubleBindPanics(t *testing.T) { + src := `CHOT x = 1 IM +CHOT x = 2 IM` + tree, err := parser.New(lexer.NewLexer(src).Tokenize()).ParseCode() + if err != nil { + t.Fatal(err) + } + interp := NewInterpreter() + defer func() { + r := recover() + if r == nil { + t.Fatal("expected panic on second CHOT in same scope") + } + s, _ := r.(string) + if !strings.Contains(s, "CHOT") { + t.Fatalf("unexpected panic: %v", r) + } + }() + interp.Run(tree) +} diff --git a/go/internal/lexer/token_type.go b/go/internal/lexer/token_type.go index f470b12..070b467 100644 --- a/go/internal/lexer/token_type.go +++ b/go/internal/lexer/token_type.go @@ -2,7 +2,7 @@ package lexer import "regexp" -// TokenType представляет все возможные типы токенов в языке. +// TokenType represents token kinds for YanghoScript (Vietnamese slang keywords + ASCII ops). type TokenType string const ( @@ -13,7 +13,6 @@ const ( STRING TokenType = "STRING" COMMENT TokenType = "COMMENT" - // Operators ASSIGN TokenType = "ASSIGN" PLUS TokenType = "PLUS" MINUS TokenType = "MINUS" @@ -22,39 +21,54 @@ const ( EQUAL TokenType = "EQUAL" LESS TokenType = "LESS" GREATER TokenType = "GREATER" - LESSEQ TokenType = "LESSEQ" // <= - MOREQ TokenType = "MOREQ" // >= - NOTEQUAL TokenType = "NOTEQUAL" // != + LESSEQ TokenType = "LESSEQ" + MOREQ TokenType = "MOREQ" + NOTEQUAL TokenType = "NOTEQUAL" - // Delimiters COMMA TokenType = "COMMA" SEMICOLON TokenType = "SEMICOLON" LPAREN TokenType = "LPAREN" RPAREN TokenType = "RPAREN" LBRACE TokenType = "LBRACE" RBRACE TokenType = "RBRACE" + LBRACKET TokenType = "LBRACKET" + RBRACKET TokenType = "RBRACKET" - // Keywords RETURN TokenType = "RETURN" LOG TokenType = "LOG" IF TokenType = "IF" ELSE TokenType = "ELSE" FUNCTION TokenType = "FUNCTION" + BIND TokenType = "BIND" WHILE TokenType = "WHILE" FOR TokenType = "FOR" BREAK TokenType = "BREAK" CONTINUE TokenType = "CONTINUE" ) +// keywords: primary spellings + slang aliases → same token. var keywords = map[string]TokenType{ - "TRA": RETURN, - "IM": SEMICOLON, - "NOILIENTUC": LOG, - "NEU": IF, - "KOTHI": ELSE, - "ME": LBRACE, - "MAY": RBRACE, - "THE": FUNCTION, + // return / end statement + "TRA": RETURN, + "IM": SEMICOLON, + "DI": SEMICOLON, + // IO (side effect) + "NOILIENTUC": LOG, + "KEU": LOG, + // condition + "NEU": IF, + "THOI": ELSE, + "KOTHI": ELSE, + // blocks + "ME": LBRACE, + "MO": LBRACE, + "MAY": RBRACE, + "DONG": RBRACE, + // function / immutable bind + "THE": FUNCTION, + "HUA": FUNCTION, + "CHOT": BIND, + // comparisons (Vietnamese phrasing) "UYTIN": EQUAL, "NHIEUHON": GREATER, "ITHON": LESS, @@ -67,8 +81,6 @@ var keywords = map[string]TokenType{ "TIEPTUC": CONTINUE, } -// tokenPatterns is ordered: longer lexemes and literals must be tried before shorter/prefix -// matches (e.g. == before =, <= before <). Map iteration in Go is randomized, so a slice is required. var tokenPatterns = []struct { Type TokenType Re *regexp.Regexp @@ -87,9 +99,11 @@ var tokenPatterns = []struct { {LESS, regexp.MustCompile(`^<`)}, {GREATER, regexp.MustCompile(`^>`)}, {COMMA, regexp.MustCompile(`^,`)}, - {SEMICOLON, regexp.MustCompile(`^IM`)}, + {SEMICOLON, regexp.MustCompile(`^(IM|DI)\b`)}, {LPAREN, regexp.MustCompile(`^\(`)}, {RPAREN, regexp.MustCompile(`^\)`)}, + {LBRACKET, regexp.MustCompile(`^\[`)}, + {RBRACKET, regexp.MustCompile(`^\]`)}, {LBRACE, regexp.MustCompile(`^\{`)}, {RBRACE, regexp.MustCompile(`^\}`)}, {IDENT, regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*`)}, diff --git a/go/internal/parser/parser.go b/go/internal/parser/parser.go index 935f96a..432aee5 100644 --- a/go/internal/parser/parser.go +++ b/go/internal/parser/parser.go @@ -14,13 +14,9 @@ type Parser struct { } func New(tokens []lexer.Token) *Parser { - return &Parser{ - tokens: tokens, - pos: 0, - } + return &Parser{tokens: tokens, pos: 0} } -// Match checks if the current token matches any of the expected types func (p *Parser) match(types ...lexer.TokenType) *lexer.Token { if p.pos < len(p.tokens) { currentToken := p.tokens[p.pos] @@ -34,7 +30,6 @@ func (p *Parser) match(types ...lexer.TokenType) *lexer.Token { return nil } -// Require ensures the current token matches the expected type or raises an error func (p *Parser) require(types ...lexer.TokenType) *lexer.Token { token := p.match(types...) if token == nil { @@ -43,7 +38,6 @@ func (p *Parser) require(types ...lexer.TokenType) *lexer.Token { return token } -// Parse variable, number, or string func (p *Parser) parseVariableOrDataTypes() ast.ExpressionNode { if number := p.match(lexer.NUMBER); number != nil { val, _ := strconv.ParseFloat(number.Literal, 64) @@ -65,17 +59,78 @@ func (p *Parser) parseVariableOrDataTypes() ast.ExpressionNode { panic(fmt.Sprintf("Expected variable, number, or string at position %d, got token: %+v", p.pos, p.tokens[p.pos])) } -// Parse print statement func (p *Parser) parsePrint() ast.Node { - logToken := p.require(lexer.LOG) // Убедитесь, что токен LOG существует - + logTok := p.require(lexer.LOG) return &ast.UnarOperationNode{ - Operator: string(logToken.Type), // Используем значение токена + Operator: string(logTok.Type), Operand: p.parseFormula(), } } -// Parse primary expressions +func (p *Parser) parseParamList() []string { + params := []string{} + for p.match(lexer.IDENT) != nil { + params = append(params, p.tokens[p.pos-1].Literal) + if p.match(lexer.COMMA) == nil { + break + } + } + return params +} + +func (p *Parser) parseFunction() ast.Node { + p.require(lexer.FUNCTION) + if p.pos < len(p.tokens) && p.tokens[p.pos].Type == lexer.LPAREN { + p.require(lexer.LPAREN) + params := p.parseParamList() + p.require(lexer.RPAREN) + p.require(lexer.LBRACE) + body := p.parseContext() + p.require(lexer.RBRACE) + return &ast.FunctionDeclarationNode{Name: "", Parameters: params, Body: body} + } + + name := p.require(lexer.IDENT) + p.require(lexer.LPAREN) + params := p.parseParamList() + p.require(lexer.RPAREN) + p.require(lexer.LBRACE) + body := p.parseContext() + p.require(lexer.RBRACE) + return &ast.FunctionDeclarationNode{Name: name.Literal, Parameters: params, Body: body} +} + +func (p *Parser) parseBind() ast.Node { + p.require(lexer.BIND) + variable := p.require(lexer.IDENT) + p.require(lexer.ASSIGN) + value := p.parseFormula() + return &ast.AssignNode{ + Variable: &ast.VariableNode{Name: variable.Literal}, + Value: value, + Immutable: true, + } +} + +func (p *Parser) parseListLiteral() ast.ExpressionNode { + p.require(lexer.LBRACKET) + elements := []ast.ExpressionNode{} + if p.match(lexer.RBRACKET) != nil { + return &ast.ListNode{Elements: elements} + } + for { + elements = append(elements, p.parseFormula()) + if p.match(lexer.RBRACKET) != nil { + break + } + if p.match(lexer.COMMA) == nil { + p.require(lexer.RBRACKET) + break + } + } + return &ast.ListNode{Elements: elements} +} + func (p *Parser) parsePrimary() ast.ExpressionNode { if p.match(lexer.LPAREN) != nil { node := p.parseFormula() @@ -83,169 +138,102 @@ func (p *Parser) parsePrimary() ast.ExpressionNode { return node } - // Check for function call + if p.pos < len(p.tokens) && p.tokens[p.pos].Type == lexer.LBRACKET { + return p.parseListLiteral() + } + + if p.pos < len(p.tokens) && p.tokens[p.pos].Type == lexer.FUNCTION { + n := p.parseFunction() + fd, ok := n.(*ast.FunctionDeclarationNode) + if !ok || fd.Name != "" { + panic("expected lambda expression (THE (args) ME ... MAY)") + } + return fd + } + if ident := p.match(lexer.IDENT); ident != nil { if p.pos < len(p.tokens) && p.tokens[p.pos].Type == lexer.LPAREN { - // Parse function call return p.parseFunctionCall(ident).(ast.ExpressionNode) } - // Otherwise, it's a variable return &ast.VariableNode{Name: ident.Literal} } return p.parseVariableOrDataTypes() } -// Parse multiplication/division func (p *Parser) parseMultiplicative() ast.ExpressionNode { node := p.parsePrimary() - for { if operator := p.match(lexer.MULTIPLY, lexer.DIVIDE); operator != nil { right := p.parsePrimary() - node = &ast.BinOperationNode{ - Left: node, - Operator: operator.Literal, - Right: right, - } + node = &ast.BinOperationNode{Left: node, Operator: operator.Literal, Right: right} continue } break } - return node } -// Parse addition/subtraction func (p *Parser) parseAdditive() ast.ExpressionNode { node := p.parseMultiplicative() - for { if operator := p.match(lexer.PLUS, lexer.MINUS); operator != nil { right := p.parseMultiplicative() - node = &ast.BinOperationNode{ - Left: node, - Operator: operator.Literal, - Right: right, - } + node = &ast.BinOperationNode{Left: node, Operator: operator.Literal, Right: right} continue } break } - return node } -// Parse if-else statement func (p *Parser) parseIf() ast.ExpressionNode { - p.require(lexer.IF) // Expecting 'NEU' - p.require(lexer.LPAREN) // Ensure opening parenthesis - condition := p.parseFormula() // Parsing the condition - p.require(lexer.RPAREN) // Ensure closing parenthesis - - // Ensure that we expect a left curly brace + p.require(lexer.IF) + p.require(lexer.LPAREN) + condition := p.parseFormula() + p.require(lexer.RPAREN) if p.match(lexer.LBRACE) == nil { - panic(fmt.Sprintf("Expected '{' at position: %d", p.pos)) + panic(fmt.Sprintf("Expected block at position: %d", p.pos)) } - - trueBlock := p.parseContext() // Parsing the true block - - p.require(lexer.RBRACE) // Expecting '}' - - var falseBlock ast.ExpressionNode = nil // Initializing the false block - if p.match(lexer.ELSE) != nil { // Checking for 'KO THI' - if p.match(lexer.IF) != nil { // If 'else if' exists - p.pos -= 1 // Move back one position because we use the match method - falseBlock = p.parseIf() // Recursively parse else if + trueBlock := p.parseContext() + p.require(lexer.RBRACE) + + var falseBlock ast.ExpressionNode + if p.match(lexer.ELSE) != nil { + if p.match(lexer.IF) != nil { + p.pos-- + falseBlock = p.parseIf() } else { if p.match(lexer.LBRACE) == nil { - panic(fmt.Sprintf("Expected '{' at position: %d", p.pos)) + panic(fmt.Sprintf("Expected block at position: %d", p.pos)) } - falseBlock = p.parseContext() // Parsing the false block - - p.require(lexer.RBRACE) // Expecting '}' + falseBlock = p.parseContext() + p.require(lexer.RBRACE) } } - return &ast.IfNode{ - Condition: condition, - ThenBranch: trueBlock, - ElseBranch: falseBlock, - } + return &ast.IfNode{Condition: condition, ThenBranch: trueBlock, ElseBranch: falseBlock} } -// Parse comparison expressions (e.g., 2 UYTIN 1) func (p *Parser) parseComparison() ast.ExpressionNode { - node := p.parseAdditive() // Start with additive expressions - + node := p.parseAdditive() for { - // Match comparison operators if operator := p.match(lexer.EQUAL, lexer.LESS, lexer.GREATER, lexer.LESSEQ, lexer.MOREQ, lexer.NOTEQUAL); operator != nil { - right := p.parseAdditive() // Parse the right-hand side - node = &ast.BinOperationNode{ - Left: node, - Operator: operator.Literal, - Right: right, - } + right := p.parseAdditive() + node = &ast.BinOperationNode{Left: node, Operator: operator.Literal, Right: right} continue } break } - return node } -// Parse formula (top-level arithmetic) func (p *Parser) parseFormula() ast.ExpressionNode { return p.parseComparison() - } -// Parse assignment -func (p *Parser) parseAssignment() ast.Node { - variable := p.require(lexer.IDENT) - p.require(lexer.ASSIGN) - value := p.parseFormula() - return &ast.AssignNode{ - Variable: &ast.VariableNode{Name: variable.Literal}, - Value: value, - } -} - -// Parse function definition -func (p *Parser) parseFunction() ast.Node { - p.require(lexer.FUNCTION) // Expecting 'THE' - name := p.require(lexer.IDENT) // Function name - p.require(lexer.LPAREN) // Expecting '(' - - // Parse parameters - parameters := []string{} - for p.match(lexer.IDENT) != nil { - parameters = append(parameters, p.tokens[p.pos-1].Literal) - if p.match(lexer.COMMA) == nil { - break - } - } - - p.require(lexer.RPAREN) // Expecting ')' - p.require(lexer.LBRACE) // Expecting 'ME' - - // Parse function body - body := p.parseContext() - - p.require(lexer.RBRACE) // Expecting 'MAY' - - return &ast.FunctionDeclarationNode{ - Name: name.Literal, - Parameters: parameters, - Body: body, - } -} - -// Parse function call func (p *Parser) parseFunctionCall(name *lexer.Token) ast.Node { p.require(lexer.LPAREN) - arguments := []ast.ExpressionNode{} if p.match(lexer.RPAREN) != nil { return &ast.FunctionCallNode{Name: name.Literal, Arguments: arguments} @@ -260,41 +248,37 @@ func (p *Parser) parseFunctionCall(name *lexer.Token) ast.Node { break } } - return &ast.FunctionCallNode{Name: name.Literal, Arguments: arguments} } -// Parse return statement func (p *Parser) parseReturn() ast.Node { - p.require(lexer.RETURN) // Expecting 'TRA' + p.require(lexer.RETURN) value := p.parseFormula() - return &ast.ReturnNode{ - Value: value, - } + return &ast.ReturnNode{Value: value} } -// Extend parseContext to include function definitions func (p *Parser) parseContext() *ast.StatementsNode { statements := &ast.StatementsNode{} for p.pos < len(p.tokens) { - // Skip delimiters if p.match(lexer.SEMICOLON) != nil { continue } - // Check for end of block if p.match(lexer.RBRACE) != nil { - p.pos-- // Move back one position to allow the caller to handle RBRACE + p.pos-- break } - // Check for end of file if p.tokens[p.pos].Type == lexer.EOF { break } - // Parse statements + // Let parseIf consume THOI/KOTHI (else) for the surrounding NEU. + if p.tokens[p.pos].Type == lexer.ELSE { + break + } + switch p.tokens[p.pos].Type { case lexer.LOG: statements.Statements = append(statements.Statements, p.parsePrint()) @@ -302,26 +286,29 @@ func (p *Parser) parseContext() *ast.StatementsNode { statements.Statements = append(statements.Statements, p.parseIf()) case lexer.FUNCTION: statements.Statements = append(statements.Statements, p.parseFunction()) - case lexer.RETURN: // Handle return statements + case lexer.RETURN: statements.Statements = append(statements.Statements, p.parseReturn()) + case lexer.BIND: + statements.Statements = append(statements.Statements, p.parseBind()) case lexer.IDENT: - // Check if it's a function call or assignment name := p.tokens[p.pos] + if p.pos+1 < len(p.tokens) && p.tokens[p.pos+1].Type == lexer.ASSIGN { + panic("immutable bind: use CHOT name = value (không gán lại bằng '=' trần)") + } if p.pos+1 < len(p.tokens) && p.tokens[p.pos+1].Type == lexer.LPAREN { - p.pos++ // Move to LPAREN + p.pos++ statements.Statements = append(statements.Statements, p.parseFunctionCall(&name)) } else { - statements.Statements = append(statements.Statements, p.parseAssignment()) + panic(fmt.Sprintf("expected call or CHOT at position %d", p.pos)) } default: - statements.Statements = append(statements.Statements, p.parseAssignment()) + panic(fmt.Sprintf("unexpected token in block: %v at %d", p.tokens[p.pos].Type, p.pos)) } } return statements } -// Parse the entire program func (p *Parser) ParseCode() (node *ast.StatementsNode, err error) { if len(p.tokens) == 0 { return nil, fmt.Errorf("no tokens to parse") diff --git a/go/internal/version/version.go b/go/internal/version/version.go index 31646b0..61c3eac 100644 --- a/go/internal/version/version.go +++ b/go/internal/version/version.go @@ -1,3 +1,3 @@ package version -const Version = "0.1.0" +const Version = "0.2.0-go" diff --git a/go/yanghoscript b/go/yanghoscript deleted file mode 100755 index 2537523..0000000 Binary files a/go/yanghoscript and /dev/null differ diff --git a/scripts/run-yanghoscript b/scripts/run-yanghoscript new file mode 100755 index 0000000..1631a67 --- /dev/null +++ b/scripts/run-yanghoscript @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Always runs the Go interpreter from this repo (avoids an old global `yanghoscript` on PATH). +# `go -C` runs the program with cwd=go/, so relative paths to .ys are resolved from your shell cwd first. +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +args=() +for a in "$@"; do + if [[ "$a" == *.ys ]] && [[ "$a" != /* ]]; then + args+=("$PWD/$a") + else + args+=("$a") + fi +done +exec env CGO_ENABLED=0 go -C "$ROOT/go" run ./cmd/yanghoscript "${args[@]}"