Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions .github/scripts/apply_issue_7.py
Original file line number Diff line number Diff line change
@@ -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 {",
)
261 changes: 261 additions & 0 deletions .github/scripts/apply_issue_7_tests_docs.py
Original file line number Diff line number Diff line change
@@ -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 -->",
'''<!-- ellipsis-token -->
## 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",
"<!-- ellipsis-type -->",
'''<!-- ellipsis-type -->
## `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 -->",
'''<!-- ellipsis-atom -->
## 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-semantics -->",
'''<!-- ellipsis-semantics -->
## 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-codegen -->",
'''<!-- ellipsis-codegen -->
## 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-builtin -->",
'''<!-- ellipsis-builtin -->
## `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-compatibility -->",
'''<!-- ellipsis-compatibility -->
## Ellipsis

minipy supports `...`, the `Ellipsis` singleton, `EllipsisType`, and
identity/equality comparisons. Ellipsis subscript expansion remains unsupported.
''',
)
Loading
Loading