Repository: goplus/gop
Pull request #2007 (2024-11-07 03:39:14)

number with unit: scanner/parser/format


Patch:
diff --git a/ast/ast_gop.go b/ast/ast_gop.go
index da6227a5..87e86b6a 100644
--- a/ast/ast_gop.go
+++ b/ast/ast_gop.go
@@ -85,6 +85,26 @@ func NextPartPos(pos token.Pos, part any) (nextPos token.Pos) {
 
 // -----------------------------------------------------------------------------
 
+// A NumberUnitLit node represents a number with unit.
+type NumberUnitLit struct {
+	ValuePos token.Pos   // literal position
+	Kind     token.Token // token.INT or token.FLOAT
+	Value    string      // literal string of the number; e.g. 42, 0x7f, 3.14, 1e-9
+	Unit     string      // unit string of the number; e.g. "px", "em", "rem"
+}
+
+func (*NumberUnitLit) exprNode() {}
+
+func (x *NumberUnitLit) Pos() token.Pos {
+	return x.ValuePos
+}
+
+func (x *NumberUnitLit) End() token.Pos {
+	return token.Pos(int(x.ValuePos) + len(x.Value) + len(x.Unit))
+}
+
+// -----------------------------------------------------------------------------
+
 // A EnvExpr node represents a ${name} expression.
 type EnvExpr struct {
 	TokPos token.Pos // position of "$"
diff --git a/doc/classfile.md b/doc/classfile.md
index 7829c3d7..0e7bc2ee 100644
--- a/doc/classfile.md
+++ b/doc/classfile.md
@@ -130,7 +130,7 @@ What is a `classfile`? Usually it consists of a `project class` and multiple `wo
 
 This is a bit abstract. Let's take the [2D Game Engine spx](https://github.com/goplus/spx) as an example. The base class of project class of `spx classfile` is called `Game`. The base class of worker class is called `Sprite`. Obviously, there will only be one Game instance in a game, but there are many types of sprites, so many types of worker classes are needed, but they all have the same base class called `Sprite`. Go+'s classfile allows you to specify different base classes for different worker classes. Although this is rare, it can be done.
 
-How does Go+ identify various class files of a classfile? by its filename. By convention, if we define a classfile called `foo`, then its project class is usually called `main_foo.gox`, and the worker class is usually called `xxx_foo.gox`. If this classfile does not have a worker class, then the project class only needs to ensure that the suffix is `_foo.gox`, and the class name can be freely chosen.
+How does Go+ identify various class files of a classfile? by its filename. By convention, if we define a classfile called `foo`, then its project class is usually called `main_foo.gox`, and the worker class is usually called `xxx_foo.gox`. If this classfile does not have a worker class, then the project class only needs to ensure that the suffix is `_foo.gox`, and the class name can be freely chosen. For example, our previous `yap` framework only has project class, so a file name like `hello_yap.gox` can be correctly recognized.
 
 The earliest version of Go+ allows classfiles to be identified through custom file extensions. For example, the project class of the `spx classfile` is called `main.spx`, and the worker class is called `xxx.spx`. Although this ability to customize extensions is still retained for now, we do not recommend its use and there is no guarantee that it will continue to be available in the future.
 
diff --git a/go.mod b/go.mod
index bf802983..3262118e 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,8 @@ module github.com/goplus/gop
 go 1.18
 
 require (
-	github.com/fsnotify/fsnotify v1.8.0
-	github.com/goplus/gogen v1.16.1
+	github.com/fsnotify/fsnotify v1.7.0
+	github.com/goplus/gogen v1.16.0
 	github.com/goplus/llgo v0.9.7
 	github.com/goplus/mod v0.13.12
 	github.com/qiniu/x v1.13.10
diff --git a/go.sum b/go.sum
index 579abc97..1b2846d7 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,7 @@
-github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
-github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
-github.com/goplus/gogen v1.16.1 h1:f55LIfyTO5GcXxCfwXKqLKiQV3AORvn9PSGCncWvB1A=
-github.com/goplus/gogen v1.16.1/go.mod h1:6TQYbabXDF9LCdDkOOzHmfg1R4ENfXQ3XpHa9RhTSD8=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/goplus/gogen v1.16.0 h1:hAK2ZX8vCjH+Y2QoJl9viSZ8Gw9pzE0vCz5voYBYnv4=
+github.com/goplus/gogen v1.16.0/go.mod h1:92qEzVgv7y8JEFICWG9GvYI5IzfEkxYdsA1DbmnTkqk=
 github.com/goplus/llgo v0.9.7 h1:LRF2Fq9ts4QrVxOPZufexalbIoJ1oiBERjCWQ45wxbg=
 github.com/goplus/llgo v0.9.7/go.mod h1:5Fs+08NslqofJ7xtOiIXugkurYOoQvY02ZkFNWA1uEI=
 github.com/goplus/mod v0.13.12 h1:Trwk6j3i9VvBuW6/9ZxmkoFlEL2v3HKQu0Na1c6DAdw=
diff --git a/parser/parser.go b/parser/parser.go
index 9b8ea67f..3ba5f455 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -1729,11 +1729,25 @@ func (p *parser) parseOperand(lhs, allowTuple, allowCmd bool) (x ast.Expr, isTup
 		if p.tok == token.STRING && len(p.lit) > 1 {
 			bl.Extra = p.stringLit(p.pos, p.lit)
 		}
-		x = bl
-		if debugParseOutput {
-			log.Printf("ast.BasicLit{Kind: %v, Value: %v}\n", p.tok, p.lit)
-		}
 		p.next()
+		if p.tok == token.UNIT {
+			nu := &ast.NumberUnitLit{
+				ValuePos: bl.ValuePos,
+				Kind:     bl.Kind,
+				Value:    bl.Value,
+				Unit:     p.lit,
+			}
+			x = nu
+			if debugParseOutput {
+				log.Printf("ast.NumberUnitLit{Kind: %v, Value: %v, Unit: %v}\n", nu.Kind, nu.Value, nu.Unit)
+			}
+			p.next()
+		} else {
+			x = bl
+			if debugParseOutput {
+				log.Printf("ast.BasicLit{Kind: %v, Value: %v}\n", bl.Kind, bl.Value)
+			}
+		}
 		return
 
 	case token.LPAREN:
@@ -2168,6 +2182,7 @@ func (p *parser) checkExpr(x ast.Expr) ast.Expr {
 	case *ast.BadExpr:
 	case *ast.Ident:
 	case *ast.BasicLit:
+	case *ast.NumberUnitLit:
 	case *ast.FuncLit:
 	case *ast.CompositeLit:
 	case *ast.SliceLit:
@@ -2942,12 +2957,12 @@ func (p *parser) parseForPhraseCond() (init ast.Stmt, cond ast.Expr) {
 	var condStmt ast.Stmt
 	var semi struct {
 		pos token.Pos
-		lit string // ";" or "\n"; valid if pos.IsValid()
+		//lit string // ";" or "\n"; valid if pos.IsValid()
 	}
 	if !isForPhraseCondEnd(p.tok) {
 		if p.tok == token.SEMICOLON {
 			semi.pos = p.pos
-			semi.lit = p.lit
+			//semi.lit = p.lit
 			p.next()
 		} else {
 			p.expect(token.SEMICOLON)
@@ -3246,6 +3261,8 @@ func (p *parser) toIdent(e ast.Expr) *ast.Ident {
 		return v
 	case *ast.BasicLit:
 		p.errorExpected(e.Pos(), fmt.Sprintf("'IDENT', found %v", v.Value), 2)
+	case *ast.NumberUnitLit:
+		p.errorExpected(e.Pos(), fmt.Sprintf("'IDENT', found %v", v.Value+v.Unit), 2)
 	default:
 		p.errorExpected(e.Pos(), "'IDENT'", 2)
 	}
diff --git a/printer/nodes.go b/printer/nodes.go
index 2cf61f61..7e7c2a08 100644
--- a/printer/nodes.go
+++ b/printer/nodes.go
@@ -825,6 +825,10 @@ func (p *printer) expr1(expr ast.Expr, prec1, depth int) {
 	case *ast.BasicLit:
 		p.print(x)
 
+	case *ast.NumberUnitLit:
+		p.print(&ast.BasicLit{Kind: x.Kind, Value: x.Value})
+		p.print(&ast.Ident{Name: x.Unit})
+
 	case *ast.FuncLit:
 		p.print(x.Type.Pos(), token.FUNC)
 		// See the comment in funcDecl about how the header size is computed.
diff --git a/scanner/scanner.go b/scanner/scanner.go
index 3c791483..4d4ff8cc 100644
--- a/scanner/scanner.go
+++ b/scanner/scanner.go
@@ -55,6 +55,7 @@ type Scanner struct {
 	lineOffset int  // current line offset
 	nParen     int
 	insertSemi bool // insert a semicolon before next newline
+	needUnit   bool
 
 	// public state - ok to modify
 	ErrorCount int // number of errors encountered
@@ -509,6 +510,8 @@ func (s *Scanner) scanNumber() (token.Token, string) {
 	} else if s.ch == 'r' {
 		tok = token.RAT
 		s.next()
+	} else if s.ch >= 'a' && s.ch <= 'z' || s.ch >= 'A' && s.ch <= 'Z' {
+		s.needUnit = true
 	}
 
 	lit := string(s.src[offs:s.offset])
@@ -828,6 +831,12 @@ scanAgain:
 
 	// determine token value
 	insertSemi := false
+	if s.needUnit { // number with unit
+		insertSemi = true
+		tok, lit = token.UNIT, s.scanIdentifier()
+		s.needUnit = false
+		goto done
+	}
 	switch ch := s.ch; {
 	case isLetter(ch):
 		lit = s.scanIdentifier()
@@ -1021,9 +1030,10 @@ scanAgain:
 			lit = string(ch)
 		}
 	}
+
+done:
 	if s.mode&dontInsertSemis == 0 {
 		s.insertSemi = insertSemi
 	}
-
 	return
 }
diff --git a/token/token.go b/token/token.go
index 56e3a958..aaa2a07a 100644
--- a/token/token.go
+++ b/token/token.go
@@ -142,11 +142,13 @@ const (
 	TILDE // additional tokens, handled in an ad-hoc manner
 	additional_end
 	additional_end2
+	additional_end3
 
 	additional_literal_beg = 96
 	additional_literal_end = 97
 
-	ENV = additional_end2 // ${name}
+	ENV  = additional_end2 // ${name}
+	UNIT = additional_end3 // 1m, 2s, 3ms, 4us, 5ns, 6m, 7h, 8d, 9w, 10y
 
 	PYSTRING = additional_literal_beg // py"Hello"
 


Test patch:
diff --git a/parser/_testdata/unit/parser.expect b/parser/_testdata/unit/parser.expect
new file mode 100644
index 00000000..e71a0367
--- /dev/null
+++ b/parser/_testdata/unit/parser.expect
@@ -0,0 +1,26 @@
+package main
+
+file step.gop
+noEntrypoint
+ast.FuncDecl:
+  Name:
+    ast.Ident:
+      Name: main
+  Type:
+    ast.FuncType:
+      Params:
+        ast.FieldList:
+  Body:
+    ast.BlockStmt:
+      List:
+        ast.ExprStmt:
+          X:
+            ast.CallExpr:
+              Fun:
+                ast.Ident:
+                  Name: step
+              Args:
+                ast.NumberUnitLit:
+                  Kind: INT
+                  Value: 1
+                  Unit: cm
diff --git a/parser/_testdata/unit/step.gop b/parser/_testdata/unit/step.gop
new file mode 100644
index 00000000..5588cd87
--- /dev/null
+++ b/parser/_testdata/unit/step.gop
@@ -0,0 +1,1 @@
+step 1cm
diff --git a/parser/parser_test.go b/parser/parser_test.go
index e0815f3b..94e1dbab 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -339,4 +339,10 @@ func (a T) +{}
 `, `/foo/bar.gop:1:6: overload operator can only have one parameter`, ``)
 }
 
+func TestNumberUnitLit(t *testing.T) {
+	var p parser
+	p.checkExpr(&ast.NumberUnitLit{})
+	p.toIdent(&ast.NumberUnitLit{})
+}
+
 // -----------------------------------------------------------------------------