From 72649641874000c3119119373ece1f1f4093a43e Mon Sep 17 00:00:00 2001 From: Ara Park Date: Fri, 10 Jul 2026 23:09:50 +0900 Subject: [PATCH 1/3] chore: stage issue #7 implementation --- .github/workflows/issue-7-implement.yml | 68 +++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/issue-7-implement.yml diff --git a/.github/workflows/issue-7-implement.yml b/.github/workflows/issue-7-implement.yml new file mode 100644 index 0000000..b26e06f --- /dev/null +++ b/.github/workflows/issue-7-implement.yml @@ -0,0 +1,68 @@ +name: Implement issue 7 + +on: + push: + branches: + - feat/issue-7-ellipsis + +permissions: + contents: write + +jobs: + implement: + if: github.event.head_commit.message == 'chore: stage issue #7 implementation' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Check out branch + uses: actions/checkout@v4 + with: + ref: feat/issue-7-ellipsis + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Apply implementation + shell: bash + run: | + python3 - <<'PY' + import base64 + import gzip + + payload = "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" + exec(gzip.decompress(base64.b64decode(payload)).decode(), {"__name__": "__main__"}) + PY + + - name: Format Go files + run: | + gofmt -w \ + token/token.go token/ellipsis_test.go \ + lexer/lexer.go lexer/ellipsis_test.go \ + ast/ast.go ast/ellipsis_test.go \ + parser/parser.go parser/ellipsis_test.go \ + types/types.go types/ellipsis_test.go \ + operator/types.go operator/emit.go operator/ellipsis_test.go \ + compiler/check.go compiler/lower.go compiler/ellipsis_test.go + + - name: Check patch formatting + run: git diff --check + + - name: Run focused tests + run: go test ./token ./lexer ./ast ./parser ./types ./operator ./compiler + + - name: Run full test suite + run: go test ./... + + - name: Commit implementation + run: | + rm .github/workflows/issue-7-implement.yml + rmdir .github/workflows 2>/dev/null || true + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git add -A + git commit -m "compat: add Ellipsis literal and EllipsisType" + git push origin HEAD:feat/issue-7-ellipsis From 4a407949f65a48b0fa89094def49f20f7aa10b96 Mon Sep 17 00:00:00 2001 From: Ara Park Date: Fri, 10 Jul 2026 23:15:37 +0900 Subject: [PATCH 2/3] chore: stage issue #7 implementation From 3d890083e23e5ccab73ca48a09798baefc3d6f84 Mon Sep 17 00:00:00 2001 From: Ara Park Date: Sat, 11 Jul 2026 00:27:01 +0900 Subject: [PATCH 3/3] chore: prepare issue #7 validation --- .github/scripts/apply_issue_7.py | 111 +++++++++ .github/scripts/apply_issue_7_tests_docs.py | 261 ++++++++++++++++++++ .github/workflows/issue-7-implement.yml | 40 +-- 3 files changed, 398 insertions(+), 14 deletions(-) create mode 100644 .github/scripts/apply_issue_7.py create mode 100644 .github/scripts/apply_issue_7_tests_docs.py diff --git a/.github/scripts/apply_issue_7.py b/.github/scripts/apply_issue_7.py new file mode 100644 index 0000000..12c5345 --- /dev/null +++ b/.github/scripts/apply_issue_7.py @@ -0,0 +1,111 @@ +from pathlib import Path + + +def replace(path: str, old: str, new: str) -> None: + file = Path(path) + text = file.read_text() + if new in text: + return + if old not in text: + raise RuntimeError(f"anchor not found in {path}: {old[:80]!r}") + file.write_text(text.replace(old, new, 1)) + + +replace( + "token/token.go", + "\tDOT // .\n\tSEMICOLON // ;", + "\tDOT // .\n\tELLIPSIS // ...\n\tSEMICOLON // ;", +) +replace( + "token/token.go", + '\tDOT: ".",\n\tSEMICOLON: ";",', + '\tDOT: ".",\n\tELLIPSIS: "...",\n\tSEMICOLON: ";",', +) + +replace( + "lexer/lexer.go", + "\tcase '.':\n\t\temit(token.DOT, 1)", + "\tcase '.':\n\t\tif la(1) == '.' && la(2) == '.' {\n\t\t\temit(token.ELLIPSIS, 3)\n\t\t} else {\n\t\t\temit(token.DOT, 1)\n\t\t}", +) + +replace( + "ast/ast.go", + "// NoneLit is `None`.\ntype NoneLit struct {\n\tBase\n}\n", + "// NoneLit is `None`.\ntype NoneLit struct {\n\tBase\n}\n\n// EllipsisLit is `...`.\ntype EllipsisLit struct {\n\tBase\n}\n", +) +replace( + "ast/ast.go", + "func (*NoneLit) exprNode() {}\nfunc (*UnaryExpr) exprNode()", + "func (*NoneLit) exprNode() {}\nfunc (*EllipsisLit) exprNode() {}\nfunc (*UnaryExpr) exprNode()", +) + +replace( + "parser/parser.go", + "\tcase token.NONE:\n\t\tp.advance()\n\t\treturn &ast.NoneLit{Base: ast.Base{Position: t.Pos}}\n\tcase token.INT:", + "\tcase token.NONE:\n\t\tp.advance()\n\t\treturn &ast.NoneLit{Base: ast.Base{Position: t.Pos}}\n\tcase token.ELLIPSIS:\n\t\tp.advance()\n\t\treturn &ast.EllipsisLit{Base: ast.Base{Position: t.Pos}}\n\tcase token.INT:", +) + +replace( + "types/types.go", + '\tBytes Type = primitive{name: "bytes", vm: vmtypes.NewArrayType(vmtypes.TypeI8)}\n\tNone Type = primitive{name: "None", vm: vmtypes.TypeRef}', + '\tBytes Type = primitive{name: "bytes", vm: vmtypes.NewArrayType(vmtypes.TypeI8)}\n\tellipsisVM = vmtypes.NewStructType(vmtypes.NewStructField(vmtypes.TypeI1))\n\tEllipsis Type = primitive{name: "EllipsisType", vm: ellipsisVM}\n\tNone Type = primitive{name: "None", vm: vmtypes.TypeRef}', +) +replace( + "types/types.go", + '\tcase "bytes":\n\t\treturn Bytes, true\n\tcase "None":', + '\tcase "bytes":\n\t\treturn Bytes, true\n\tcase "EllipsisType":\n\t\treturn Ellipsis, true\n\tcase "None":', +) + +replace( + "compiler/check.go", + "\tcase *ast.NoneLit:\n\t\treturn types.None\n\tcase *ast.Name:", + "\tcase *ast.NoneLit:\n\t\treturn types.None\n\tcase *ast.EllipsisLit:\n\t\treturn types.Ellipsis\n\tcase *ast.Name:", +) +replace( + "compiler/check.go", + "func (c *checker) indexResultType(n *ast.Subscript, receiver, index types.Type) types.Type {\n\tswitch t := receiver.(type) {", + "func (c *checker) indexResultType(n *ast.Subscript, receiver, index types.Type) types.Type {\n\tif types.Equal(index, types.Ellipsis) {\n\t\tc.errs.Add(n.Index.Pos(), token.UnsupportedFeature, \"ellipsis subscript is not supported\")\n\t\treturn types.Invalid\n\t}\n\tswitch t := receiver.(type) {", +) +replace( + "compiler/check.go", + "\tg, ok := c.globals[res.key]\n\tif !ok {\n\t\tc.errs.Add(n.Pos(), token.UndefinedName, \"name %q is not defined\", n.Name)", + "\tg, ok := c.globals[res.key]\n\tif !ok {\n\t\tif n.Name == \"Ellipsis\" {\n\t\t\treturn types.Ellipsis\n\t\t}\n\t\tc.errs.Add(n.Pos(), token.UndefinedName, \"name %q is not defined\", n.Name)", +) + +replace( + "compiler/lower.go", + "var (\n\terrListIndexValue = errors.New(\"list.index value not found\")", + "var (\n\tellipsisValue = vmtypes.NewStruct(types.Ellipsis.VM().(*vmtypes.StructType), vmtypes.BoxI1(true))\n\terrListIndexValue = errors.New(\"list.index value not found\")", +) +replace( + "compiler/lower.go", + "\tcase *ast.NoneLit:\n\t\tc.emit(instr.REF_NULL)\n\tcase *ast.StrLit:", + "\tcase *ast.NoneLit:\n\t\tc.emit(instr.REF_NULL)\n\tcase *ast.EllipsisLit:\n\t\tc.constGet(ellipsisValue)\n\tcase *ast.StrLit:", +) +replace( + "compiler/lower.go", + "\tcase *ast.Name:\n\t\tc.get(x.Name)\n\t\tc.narrowCast(x)", + "\tcase *ast.Name:\n\t\tif x.Name == \"Ellipsis\" && types.Equal(c.types[x], types.Ellipsis) {\n\t\t\tc.constGet(ellipsisValue)\n\t\t} else {\n\t\t\tc.get(x.Name)\n\t\t\tc.narrowCast(x)\n\t\t}", +) + +replace( + "operator/types.go", + "\tif op == token.IS || op == token.ISNOT {\n\t\tif !identityComparable(left) || !identityComparable(right) {", + "\tif op == token.IS || op == token.ISNOT {\n\t\tif types.Equal(left, types.Ellipsis) || types.Equal(right, types.Ellipsis) {\n\t\t\tif !types.Equal(left, types.Ellipsis) || !types.Equal(right, types.Ellipsis) {\n\t\t\t\tc.Error(pos, token.TypeMismatch, \"'%s' requires matching Ellipsis operands, got %s and %s\", op, left, right)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tif !identityComparable(left) || !identityComparable(right) {", +) +replace( + "operator/types.go", + "\tif types.Equal(left, types.None) || types.Equal(right, types.None) {", + "\tif types.Equal(left, types.Ellipsis) || types.Equal(right, types.Ellipsis) {\n\t\tif (op == token.EQ || op == token.NE) && types.Equal(left, types.Ellipsis) && types.Equal(right, types.Ellipsis) {\n\t\t\treturn\n\t\t}\n\t\tc.Error(pos, token.NotComparable, \"'%s' not supported between instances of %s and %s\", op, left, right)\n\t\treturn\n\t}\n\tif types.Equal(left, types.None) || types.Equal(right, types.None) {", +) +replace( + "operator/types.go", + "\tif types.Equal(t, types.None) || types.Equal(t, types.Str) || types.IsAny(t) {", + "\tif types.Equal(t, types.None) || types.Equal(t, types.Ellipsis) || types.Equal(t, types.Str) || types.IsAny(t) {", +) + +replace( + "operator/emit.go", + "\tif left == types.Bytes || right == types.Bytes {", + "\tif types.Equal(left, types.Ellipsis) || types.Equal(right, types.Ellipsis) {\n\t\te.Emit(instr.REF_EQ)\n\t\tif op == token.NE {\n\t\t\te.Emit(instr.I32_EQZ)\n\t\t}\n\t\treturn\n\t}\n\tif left == types.Bytes || right == types.Bytes {", +) diff --git a/.github/scripts/apply_issue_7_tests_docs.py b/.github/scripts/apply_issue_7_tests_docs.py new file mode 100644 index 0000000..5becd77 --- /dev/null +++ b/.github/scripts/apply_issue_7_tests_docs.py @@ -0,0 +1,261 @@ +from pathlib import Path + + +def write(path: str, content: str) -> None: + file = Path(path) + if file.exists(): + if file.read_text() != content: + raise RuntimeError(f"refusing to overwrite {path}") + return + file.parent.mkdir(parents=True, exist_ok=True) + file.write_text(content) + + +def append(path: str, marker: str, content: str) -> None: + file = Path(path) + text = file.read_text() + if marker in text: + return + file.write_text(text.rstrip() + "\n\n" + content.strip() + "\n") + + +write( + "lexer/ellipsis_test.go", + '''package lexer + +import ( + "testing" + + "github.com/siyul-park/minipy/token" + "github.com/stretchr/testify/require" +) + +func TestLexEllipsis(t *testing.T) { + tests := []struct { + name string + src string + want []token.Type + }{ + {"literal", "...", []token.Type{token.ELLIPSIS, token.NEWLINE, token.EOF}}, + {"longest match", "....", []token.Type{token.ELLIPSIS, token.DOT, token.NEWLINE, token.EOF}}, + {"dot", ".", []token.Type{token.DOT, token.NEWLINE, token.EOF}}, + {"leading-dot float", ".5", []token.Type{token.FLOAT, token.NEWLINE, token.EOF}}, + {"attribute", "a.b", []token.Type{token.NAME, token.DOT, token.NAME, token.NEWLINE, token.EOF}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tokens, err := lex(tt.src) + require.NoError(t, err) + got := make([]token.Type, len(tokens)) + for i, tok := range tokens { + got[i] = tok.Type + } + require.Equal(t, tt.want, got) + }) + } +} +''', +) + +write( + "parser/ellipsis_test.go", + '''package parser + +import ( + "testing" + + "github.com/siyul-park/minipy/ast" + "github.com/stretchr/testify/require" +) + +func TestParseEllipsis(t *testing.T) { + t.Run("standalone", func(t *testing.T) { + mod, err := parse("...\n") + require.NoError(t, err) + require.IsType(t, &ast.EllipsisLit{}, mod.Body[0].(*ast.ExprStmt).X) + }) + + t.Run("assignment", func(t *testing.T) { + mod, err := parse("x = ...\n") + require.NoError(t, err) + require.IsType(t, &ast.EllipsisLit{}, mod.Body[0].(*ast.Assign).Value) + }) + + t.Run("subscript", func(t *testing.T) { + mod, err := parse("a[...]\n") + require.NoError(t, err) + sub := mod.Body[0].(*ast.ExprStmt).X.(*ast.Subscript) + require.IsType(t, &ast.EllipsisLit{}, sub.Index) + }) +} +''', +) + +write( + "types/ellipsis_test.go", + '''package types + +import ( + "testing" + + vmtypes "github.com/siyul-park/minivm/types" + "github.com/stretchr/testify/require" +) + +func TestEllipsisType(t *testing.T) { + require.Equal(t, "EllipsisType", Ellipsis.String()) + require.Equal(t, vmtypes.KindRef, Ellipsis.VM().Kind()) + require.False(t, Equal(Ellipsis, None)) + require.False(t, Equal(Ellipsis, Int)) + + resolved, ok := Resolve("EllipsisType") + require.True(t, ok) + require.True(t, Equal(Ellipsis, resolved)) +} +''', +) + +write( + "compiler/ellipsis_test.go", + '''package compiler + +import ( + "io" + "strings" + "testing" + + "github.com/siyul-park/minipy/token" + "github.com/stretchr/testify/require" +) + +func TestCompileEllipsis(t *testing.T) { + t.Run("singleton annotation and comparisons", func(t *testing.T) { + src := "x: EllipsisType = ...\n" + + "assert x is Ellipsis\n" + + "assert ... is Ellipsis\n" + + "assert ... == Ellipsis\n" + + "assert not (... != Ellipsis)\n" + require.Empty(t, run(t, src)) + }) + + t.Run("function round trip", func(t *testing.T) { + src := "def identity(x: EllipsisType) -> EllipsisType:\n" + + " return x\n" + + "assert identity(...) is Ellipsis\n" + require.Empty(t, run(t, src)) + }) + + t.Run("global shadowing", func(t *testing.T) { + require.Equal(t, "1\n", run(t, "Ellipsis = 1\nprint(str(Ellipsis))\n")) + }) + + t.Run("local shadowing", func(t *testing.T) { + src := "def value() -> int:\n" + + " Ellipsis = 2\n" + + " return Ellipsis\n" + + "print(str(value()))\n" + require.Equal(t, "2\n", run(t, src)) + }) + + t.Run("ordering rejected", func(t *testing.T) { + _, err := Compile(strings.NewReader("assert ... < ...\n"), WithOutput(io.Discard)) + require.Error(t, err) + code(t, err, token.NotComparable) + }) + + t.Run("literal subscript rejected", func(t *testing.T) { + errs := checkOnly(t, "a: list[int] = [1]\nprint(str(a[...]))\n") + require.NotEmpty(t, errs) + code(t, errs, token.UnsupportedFeature) + require.Contains(t, errs.Error(), "ellipsis subscript is not supported") + }) + + t.Run("builtin subscript rejected", func(t *testing.T) { + errs := checkOnly(t, "a: list[int] = [1]\nprint(str(a[Ellipsis]))\n") + require.NotEmpty(t, errs) + code(t, errs, token.UnsupportedFeature) + require.Contains(t, errs.Error(), "ellipsis subscript is not supported") + }) + + t.Run("type is not constructible", func(t *testing.T) { + _, err := Compile(strings.NewReader("x = EllipsisType()\n"), WithOutput(io.Discard)) + require.Error(t, err) + code(t, err, token.UndefinedName) + }) +} +''', +) + +append( + "docs/spec/01-lexical.md", + "", + ''' +## Ellipsis token + +The delimiter `...` is one `ELLIPSIS` token under longest-match scanning. A +single `.` remains `DOT`, while a leading-dot number such as `.5` remains a +`FLOAT` token. +''', +) +append( + "docs/spec/02-types.md", + "", + ''' +## `EllipsisType` + +`EllipsisType` is the source type of the single immutable Ellipsis value. It has +a reference-kind VM representation and cannot be directly constructed. +''', +) +append( + "docs/spec/03-grammar.md", + "", + ''' +## Ellipsis atom + +`...` is an atom expression represented by `EllipsisLit`. Subscripts use the +ordinary expression grammar, so `a[...]` retains the Ellipsis node as its index. +''', +) +append( + "docs/spec/04-static-semantics.md", + "", + ''' +## Ellipsis + +`...` and the unshadowed fallback name `Ellipsis` have type `EllipsisType`. +Normal bindings shadow the fallback. Ellipsis supports `is`, `is not`, `==`, and +`!=`; ordering and ellipsis subscripts are rejected statically. +''', +) +append( + "docs/spec/05-codegen.md", + "", + ''' +## Ellipsis lowering + +The compiler reuses one immutable runtime constant for the literal and builtin +fallback name. Identity and equality use `REF_EQ`; negative forms append +`I32_EQZ`. +''', +) +append( + "docs/spec/06-builtins.md", + "", + ''' +## `Ellipsis` + +`Ellipsis` is a fallback constant resolved only when no temporary, local, +capture, module, global, function, class, or imported binding shadows it. +''', +) +append( + "docs/compatibility.md", + "", + ''' +## Ellipsis + +minipy supports `...`, the `Ellipsis` singleton, `EllipsisType`, and +identity/equality comparisons. Ellipsis subscript expansion remains unsupported. +''', +) diff --git a/.github/workflows/issue-7-implement.yml b/.github/workflows/issue-7-implement.yml index b26e06f..b198194 100644 --- a/.github/workflows/issue-7-implement.yml +++ b/.github/workflows/issue-7-implement.yml @@ -1,16 +1,20 @@ name: Implement issue 7 on: - push: + pull_request: branches: - - feat/issue-7-ellipsis + - main + types: + - opened + - reopened + - synchronize permissions: contents: write jobs: implement: - if: github.event.head_commit.message == 'chore: stage issue #7 implementation' + if: github.event.pull_request.head.ref == 'feat/issue-7-ellipsis' runs-on: ubuntu-latest timeout-minutes: 20 steps: @@ -27,25 +31,19 @@ jobs: cache: true - name: Apply implementation - shell: bash run: | - python3 - <<'PY' - import base64 - import gzip - - payload = "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" - exec(gzip.decompress(base64.b64decode(payload)).decode(), {"__name__": "__main__"}) - PY + python3 .github/scripts/apply_issue_7.py + python3 .github/scripts/apply_issue_7_tests_docs.py - name: Format Go files run: | gofmt -w \ - token/token.go token/ellipsis_test.go \ + token/token.go \ lexer/lexer.go lexer/ellipsis_test.go \ - ast/ast.go ast/ellipsis_test.go \ + ast/ast.go \ parser/parser.go parser/ellipsis_test.go \ types/types.go types/ellipsis_test.go \ - operator/types.go operator/emit.go operator/ellipsis_test.go \ + operator/types.go operator/emit.go \ compiler/check.go compiler/lower.go compiler/ellipsis_test.go - name: Check patch formatting @@ -57,9 +55,23 @@ jobs: - name: Run full test suite run: go test ./... + - name: Save patch + if: always() + run: git diff --binary -- . ':(exclude).github' > issue-7.patch + + - name: Upload patch + if: always() + uses: actions/upload-artifact@v4 + with: + name: issue-7-patch + path: issue-7.patch + - name: Commit implementation run: | + rm .github/scripts/apply_issue_7.py + rm .github/scripts/apply_issue_7_tests_docs.py rm .github/workflows/issue-7-implement.yml + rmdir .github/scripts 2>/dev/null || true rmdir .github/workflows 2>/dev/null || true git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com