Repository: universal-ctags/ctags
Pull request #4243: TypeSpec: new parser
Author: login: iaalm
Labels: name: next-release
Created: 2025-05-08T10:55:46Z | Merged: 2025-05-27T09:43:07Z

Add parser for [TypeSpec](https://typespec.io)

Test passed with `make units LANGUAGES=TypeSpec`


Diff:
diff --git a/Units/parser-typespec.r/simple-typespec.d/args.ctags b/Units/parser-typespec.r/simple-typespec.d/args.ctags
new file mode 100644
index 0000000000..5ee5f79f70
--- /dev/null
+++ b/Units/parser-typespec.r/simple-typespec.d/args.ctags
@@ -0,0 +1 @@
+--sort=no
diff --git a/Units/parser-typespec.r/simple-typespec.d/expected.tags b/Units/parser-typespec.r/simple-typespec.d/expected.tags
new file mode 100644
index 0000000000..18cc37738a
--- /dev/null
+++ b/Units/parser-typespec.r/simple-typespec.d/expected.tags
@@ -0,0 +1,13 @@
+Name.Space	input.tsp	/^namespace Name.Space;$/;"	n
+Inner	input.tsp	/^namespace Inner {$/;"	n	namespace:Name.Space
+Versions	input.tsp	/^  enum Versions {$/;"	g	namespace:Name.Space.Inner
+OpA	input.tsp	/^  op OpA<T extends TypeSpec.Reflection.Model> is OpB<$/;"	o	namespace:Name.Space.Inner
+InterfaceA	input.tsp	/^  interface InterfaceA extends InterfaceB {$/;"	i	namespace:Name.Space.Inner
+opA	input.tsp	/^    opA is OpA<{$/;"	p	interface:Name.Space.Inner.InterfaceA
+opB	input.tsp	/^    opB is OpB<$/;"	p	interface:Name.Space.Inner.InterfaceA
+UnionA	input.tsp	/^  union UnionA {$/;"	u	namespace:Name.Space.Inner
+ModelA	input.tsp	/^model ModelA {$/;"	m	namespace:Name.Space
+contentA	input.tsp	/^  contentA: ModelB;$/;"	p	model:Name.Space.ModelA
+contentB	input.tsp	/^  contentB?: boolean;$/;"	p	model:Name.Space.ModelA
+ModelB	input.tsp	/^model ModelB extends ModelA {$/;"	m	namespace:Name.Space
+AliasA	input.tsp	/^alias AliasA<$/;"	a	namespace:Name.Space
diff --git a/Units/parser-typespec.r/simple-typespec.d/input.tsp b/Units/parser-typespec.r/simple-typespec.d/input.tsp
new file mode 100644
index 0000000000..50cfdeca13
--- /dev/null
+++ b/Units/parser-typespec.r/simple-typespec.d/input.tsp
@@ -0,0 +1,92 @@
+import "@typespec/http";
+import "./common.tsp";
+
+using TypeSpec;
+using TypeSpec.Rest;
+
+@armProviderNamespace
+@service(#{ title: "Name.Space" })
+@versioned(Versions)
+@armCommonTypesVersion(CommonTypes.Versions.v3)
+namespace Name.Space;
+
+namespace Inner {
+  @doc("API Versions")
+  @added(a())
+  enum Versions {
+    @doc("May 01, 2024 Preview API Version")
+    v2024_05_01_preview: "2024-05-01-preview",
+  }
+
+  @summary("Example operation summary for demonstration purposes.")
+  @doc("""
+    Example
+    multi-line
+    documentation.
+    """)
+  @returnsDoc("Example return documentation")
+  @post
+  op OpA<T extends TypeSpec.Reflection.Model> is OpB<
+    T & ModelA,
+    ModelB<ModelA[]>
+  >;
+
+  // single line comment
+  interface InterfaceA extends InterfaceB {
+    opA is OpA<{
+      @doc("The format of the HTTP payload.")
+      @header
+      contentType: "application/json";
+
+      @clientName("uri", "c\\sharp")
+      @doc("Example URL parameter.")
+      opB: url;
+    }>;
+
+    /**
+    * Example operation description
+    */
+    opB is OpB<
+      ModelA,
+      {
+        ...ModelB
+      }
+    >;
+  }
+
+  @doc("Example union type description.")
+  union UnionA {
+    string,
+
+    @doc("Example option A description with some technical details that would be typical for this kind of documentation.")
+    "unionA",
+
+    @doc("Example option B description with additional technical context that would be common in API documentation.")
+    "unionB",
+  }
+}
+
+@doc("Example model description.")
+model ModelA {
+  @doc("Example property description.")
+  contentA: ModelB;
+
+  @removed(Versions.v1_2)
+  @doc("Example boolean property with typical documentation about its purpose and default behavior.")
+  contentB?: boolean;
+
+  ...ModelX;
+}
+
+@doc("Example model description.")
+model ModelB extends ModelA {
+  ...ModelX;
+}
+
+
+alias AliasA<
+  TParamA extends ModelX,
+  TParamB,
+  TParamC extends ModelX = {},
+  TError = Error
+> = ModelB<TParamA, TParamB, TParamC, TError>;
diff --git a/docs/news/HEAD.rst b/docs/news/HEAD.rst
index 69addff975..60605dfdfa 100644
--- a/docs/news/HEAD.rst
+++ b/docs/news/HEAD.rst
@@ -45,6 +45,7 @@ New parsers
 * PythonEntryPoints *subparser*
 * Scdoc *optlib*
 * JNI *subparser*
+* TypeSpec *parser*
 
 .. note:: We added a TOML as a new parser in this version. However,
 		  after adding it, we learned its implementation didn't work
diff --git a/main/parsers_p.h b/main/parsers_p.h
index 04429a547f..4f3f06c3d0 100644
--- a/main/parsers_p.h
+++ b/main/parsers_p.h
@@ -190,6 +190,7 @@
 	TTCNParser, \
 	Txt2tagsParser, \
 	TypeScriptParser, \
+	TypeSpecParser, \
 	VParser, \
 	VeraParser, \
 	VerilogParser, \
diff --git a/parsers/typespec.c b/parsers/typespec.c
new file mode 100644
index 0000000000..7b7d2b5a81
--- /dev/null
+++ b/parsers/typespec.c
@@ -0,0 +1,682 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enumeration names" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import,
+	KEYWORD_is,
+	KEYWORD_extends
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import },
+	{ "is",		 KEYWORD_is },
+	{ "extends",	KEYWORD_extends }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	int				scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= CORK_NIL;
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+}
+
+static void setScope (tokenInfo *const token, int scope)
+{
+	token->scope = scope;
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+	} while (c != EOF && c != '\n');
+	return c;
+}
+
+static int skipMultiComment (void)
+{
+	int c = getcFromInputFile ();
+	int next = getcFromInputFile ();
+
+	while (c != EOF && !(c == '*' && next == '/'))
+	{
+		c = next;
+		next = getcFromInputFile ();
+	}
+
+	return getcFromInputFile ();
+}
+
+static void parseString (vString *const string, const int delimiter)
+{
+	while (true)
+	{
+		int c = getcFromInputFile ();
+
+		if (c == '\\' && (c = getcFromInputFile ()) != EOF)
+			vStringPut (string, c);
+		else if (c == EOF || c == delimiter)
+			break;
+		else
+			vStringPut (string, c);
+	}
+}
+
+static void parseIdentifier (vString *const string, const int firstChar)
+{
+	int c = firstChar;
+	do
+	{
+		vStringPut (string, c);
+		c = getcFromInputFile ();
+	} while (isIdentChar (c));
+	ungetcToInputFile (c);
+}
+
+static void readToken (tokenInfo *const token)
+{
+	int c;
+
+	token->type		= TOKEN_UNDEFINED;
+	vStringClear (token->string);
+
+getNextChar:
+
+	c = getcFromInputFile ();
+	c = skipWhitespace (c);
+
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	switch (c)
+	{
+		case EOF: token->type = TOKEN_EOF;					break;
+		case '(': token->type = TOKEN_OPEN_PAREN;			break;
+		case ')': token->type = TOKEN_CLOSE_PAREN;			break;
+		case ';': token->type = TOKEN_SEMICOLON;			break;
+		case ',': token->type = TOKEN_COMMA;				break;
+		case '.':
+		{
+			/* Handle ... (spread) operator */
+			int c1 = getcFromInputFile ();
+			int c2 = getcFromInputFile ();
+			if (c1 == '.' && c2 == '.')
+			{
+				token->type = TOKEN_SPREAD;
+			}
+			else
+			{
+				ungetcToInputFile (c2);
+				ungetcToInputFile (c1);
+				token->type = TOKEN_PERIOD;
+			}
+			break;
+		}
+		case ':': token->type = TOKEN_COLON;				break;
+		case '{': token->type = TOKEN_OPEN_CURLY;			break;
+		case '}': token->type = TOKEN_CLOSE_CURLY;			break;
+		case '<': token->type = TOKEN_OPEN_ANGLE;			break;
+		case '>': token->type = TOKEN_CLOSE_ANGLE;			break;
+		case '=': token->type = TOKEN_EQUAL_SIGN;			break;
+		case '[': token->type = TOKEN_OPEN_SQUARE;			break;
+		case ']': token->type = TOKEN_CLOSE_SQUARE;			break;
+
+		case '\'':
+		case '"':
+			token->type = TOKEN_STRING;
+			parseString (token->string, c);
+			break;
+
+		case '/':
+		{
+			int d = getcFromInputFile ();
+			if (d == '/')  /* single-line comment */
+			{
+				skipSingleComment ();
+				goto getNextChar;
+			}
+			else if (d == '*')  /* multi-line comment */
+			{
+				skipMultiComment ();
+				goto getNextChar;
+			}
+			else
+			{
+				ungetcToInputFile (d);
+				token->type = TOKEN_UNDEFINED;
+			}
+			break;
+		}
+
+		case '@':  /* decorator */
+			token->type = TOKEN_DECORATOR;
+			c = getcFromInputFile ();
+			parseIdentifier (token->string, c);
+			break;
+
+		default:
+			if (! isIdentChar (c))
+				token->type = TOKEN_UNDEFINED;
+			else
+			{
+				parseIdentifier (token->string, c);
+				token->keyword = lookupCaseKeyword (
+					vStringValue (token->string), getInputLanguage ());
+				if (token->keyword == KEYWORD_NONE)
+					token->type = TOKEN_IDENTIFIER;
+				else
+					token->type = TOKEN_KEYWORD;
+			}
+			break;
+	}
+}
+
+static void initTypeSpecEntry (tagEntryInfo *const e, const tokenInfo *const token,
+							  const typeSpecKind kind)
+{
+	initTagEntry (e, vStringValue (token->string), kind);
+
+	e->lineNumber	= token->lineNumber;
+	e->filePosition	= token->filePosition;
+
+	e->extensionFields.scopeIndex = token->scope;
+}
+
+static int makeTypeSpecTag (const tokenInfo *const token, const typeSpecKind kind)
+{
+	tagEntryInfo e;
+	initTypeSpecEntry (&e, token, kind);
+	return makeTagEntry (&e);
+}
+
+static void enterScope (tokenInfo *const parentToken, int scope);
+
+static void skipTypeParameters(tokenInfo *const token)
+{
+	int depth = 1;
+
+	while (depth > 0 && token->type != TOKEN_EOF)
+	{
+		readToken(token);
+
+		if (token->type == TOKEN_OPEN_ANGLE)
+			depth++;
+		else if (token->type == TOKEN_CLOSE_ANGLE)
+			depth--;
+	}
+	readToken(token);
+}
+
+static void skipToSemicolon(tokenInfo *const token)
+{
+	while (token->type != TOKEN_SEMICOLON && token->type != TOKEN_EOF)
+	{
+		readToken(token);
+	}
+}
+
+static void parseNamespace(tokenInfo *const token)
+{
+	vString *name = vStringNew();
+
+	/* Read namespace components */
+	do
+	{
+		readToken(token);
+
+		if (token->type == TOKEN_IDENTIFIER)
+		{
+			if (!vStringIsEmpty(name))
+				vStringCatS(name, SCOPE_SEPARATOR);
+			vStringCat(name, token->string);
+
+			readToken(token);
+		}
+		else
+		{
+			break;
+		}
+	} while (token->type == TOKEN_PERIOD);
+
+	/* Create namespace tag */
+	vStringCopy(token->string, name);
+	int namespaceIndex = makeTypeSpecTag(token, K_NAMESPACE);
+
+	/* Parse namespace body */
+	if (token->type == TOKEN_SEMICOLON || token->type == TOKEN_OPEN_CURLY)
+	{
+		enterScope(token, namespaceIndex);
+	}
+
+	vStringDelete(name);
+}
+
+static void parseEnum(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		int enumIndex = makeTypeSpecTag(token, K_ENUM);
+		readToken(token);
+
+		if (token->type == TOKEN_OPEN_CURLY)
+		{
+			enterScope(token, enumIndex);
+		}
+	}
+}
+
+static void parseProperty(tokenInfo *const token)
+{
+	if (token->type == TOKEN_IDENTIFIER || token->type == TOKEN_STRING)
+	{
+		/* This is a property */
+		makeTypeSpecTag(token, K_PROPERTY);
+
+		/* Skip type declaration, modifiers, etc. until ; or , */
+		while (token->type != TOKEN_SEMICOLON &&
+			   token->type != TOKEN_COMMA &&
+			   token->type != TOKEN_CLOSE_CURLY &&
+			   token->type != TOKEN_EOF)
+		{
+		/* Skip generic parameters if any */
+			readToken(token);
+			if (token->type == TOKEN_OPEN_ANGLE)
+			{
+				skipTypeParameters(token);
+			}
+		}
+	}
+}
+
+static void parseOperation(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		makeTypeSpecTag(token, K_OPERATION);
+		readToken(token);
+
+		/* Skip generic parameters if any */
+		if (token->type == TOKEN_OPEN_ANGLE)
+		{
+			skipTypeParameters(token);
+		}
+
+		/* Handle "is" relationship */
+		if (token->type == TOKEN_KEYWORD && token->keyword == KEYWORD_is)
+		{
+			/* Skip expression after "is" */
+			do {
+				readToken(token);
+
+				if (token->type == TOKEN_OPEN_ANGLE)
+				{
+					skipTypeParameters(token);
+				}
+
+				if (token->type == TOKEN_SEMICOLON)
+					break;
+
+			} while (token->type != TOKEN_SEMICOLON && token->type != TOKEN_EOF);
+		}
+	}
+}
+
+static void parseInterface(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		int ifaceIndex = makeTypeSpecTag(token, K_INTERFACE);
+		readToken(token);
+
+		/* Skip extends clause */
+		if (token->type == TOKEN_KEYWORD && token->keyword == KEYWORD_extends)
+		{
+			while (token->type != TOKEN_OPEN_CURLY && token->type != TOKEN_EOF)
+			{
+				readToken(token);
+			}
+		}
+
+		if (token->type == TOKEN_OPEN_CURLY)
+		{
+			enterScope(token, ifaceIndex);
+		}
+	}
+}
+
+static void parseModel(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		int modelIndex = makeTypeSpecTag(token, K_MODEL);
+		readToken(token);
+
+		/* Skip extends clause if any */
+		if (token->type == TOKEN_KEYWORD && token->keyword == KEYWORD_extends)
+		{
+			while (token->type != TOKEN_OPEN_CURLY && token->type != TOKEN_EOF)
+			{
+				readToken(token);
+			}
+		}
+
+		if (token->type == TOKEN_OPEN_CURLY)
+		{
+			enterScope(token, modelIndex);
+		}
+	}
+}
+
+static void parseUnion(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		int unionIndex = makeTypeSpecTag(token, K_UNION);
+		readToken(token);
+
+		if (token->type == TOKEN_OPEN_CURLY)
+		{
+			enterScope(token, unionIndex);
+		}
+	}
+}
+
+static void parseAlias(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		makeTypeSpecTag(token, K_ALIAS);
+		readToken(token);
+
+		/* Skip generic parameters if any */
+		if (token->type == TOKEN_OPEN_ANGLE)
+		{
+			skipTypeParameters(token);
+		}
+
+		/* Skip the rest until semicolon */
+		skipToSemicolon(token);
+	}
+}
+
+static void enterScope (tokenInfo *const parentToken, int scope)
+{
+	tokenInfo *token = newToken ();
+	tagEntryInfo *e_parent;
+
+	copyToken (token, parentToken);
+	setScope (token, scope);
+
+	readToken (token);
+	while (token->type != TOKEN_EOF && token->type != TOKEN_CLOSE_CURLY)
+	{
+		/* Skip decorators */
+		while (token->type == TOKEN_DECORATOR)
+		{
+			/* Skip the decorator body if it has parameters */
+			readToken(token);
+			if (token->type == TOKEN_OPEN_PAREN)
+			{
+				int depth = 1;
+				while (depth > 0 && token->type != TOKEN_EOF)
+				{
+					readToken(token);
+					if (token->type == TOKEN_OPEN_PAREN)
+						depth++;
+					else if (token->type == TOKEN_CLOSE_PAREN)
+						depth--;
+				}
+				readToken(token);
+			}
+		}
+
+		switch (token->type)
+		{
+			case TOKEN_KEYWORD:
+				switch (token->keyword)
+				{
+					case KEYWORD_namespace:
+						parseNamespace(token);
+						break;
+
+					case KEYWORD_enum:
+						parseEnum(token);
+						break;
+
+					case KEYWORD_op:
+						parseOperation(token);
+						break;
+
+					case KEYWORD_interface:
+						parseInterface(token);
+						break;
+
+					case KEYWORD_model:
+						parseModel(token);
+						break;
+
+					case KEYWORD_union:
+						parseUnion(token);
+						break;
+
+					case KEYWORD_alias:
+						parseAlias(token);
+						break;
+
+					case KEYWORD_using:
+					case KEYWORD_import:
+					case KEYWORD_is:
+					case KEYWORD_extends:
+						skipToSemicolon(token);
+						break;
+
+					default:
+						readToken(token);
+						break;
+				}
+				break;
+
+			case TOKEN_SPREAD:
+				/* Skip spread expressions (e.g. ...LivenessSessionData) */
+				readToken(token);
+				break;
+
+			case TOKEN_IDENTIFIER:
+			case TOKEN_STRING:
+				e_parent = getEntryInCorkQueue (scope);
+				if (e_parent
+					&& (e_parent->kindIndex == K_MODEL
+						|| e_parent->kindIndex == K_INTERFACE))
+				{
+					parseProperty(token);
+				}
+				break;
+
+			default:
+				break;
+		}
+
+		readToken(token);
+	}
+
+	copyToken (parentToken, token);
+	/* the scope of parentToken is not changed in this function.  */
+	deleteToken (token);
+}
+
+static void findTypeSpecTags (void)
+{
+	tokenInfo *const token = newToken ();
+
+	do
+	{
+		enterScope (token, CORK_NIL);
+	}
+	while (token->type != TOKEN_EOF);
+
+	deleteToken (token);
+}
+
+extern parserDefinition* TypeSpecParser (void)
+{
+	static const char *const extensions [] = { "tsp", NULL };
+	parserDefinition* def = parserNew ("TypeSpec");
+	def->kindTable  = TypeSpecKinds;
+	def->kindCount  = ARRAY_SIZE (TypeSpecKinds);
+	def->extensions = extensions;
+	def->parser	 = findTypeSpecTags;
+	def->keywordTable = TypeSpecKeywordTable;
+	def->keywordCount = ARRAY_SIZE (TypeSpecKeywordTable);
+	def->defaultScopeSeparator = SCOPE_SEPARATOR;
+	def->useCork = CORK_QUEUE;
+	return def;
+}
diff --git a/source.mak b/source.mak
index 06a35b105f..9b6633a5b5 100644
--- a/source.mak
+++ b/source.mak
@@ -432,6 +432,7 @@ PARSER_SRCS =				\
 	parsers/txt2tags.c		\
 	parsers/typescript.c		\
 	parsers/v.c			\
+	parsers/typespec.c		\
 	parsers/vera.c			\
 	parsers/verilog.c		\
 	parsers/vhdl.c			\
diff --git a/win32/ctags_vs2013.vcxproj b/win32/ctags_vs2013.vcxproj
index 98773be0fc..0753ff7236 100644
--- a/win32/ctags_vs2013.vcxproj
+++ b/win32/ctags_vs2013.vcxproj
@@ -378,6 +378,7 @@
     <ClCompile Include="..\parsers\ttcn.c" />
     <ClCompile Include="..\parsers\txt2tags.c" />
     <ClCompile Include="..\parsers\typescript.c" />
+    <ClCompile Include="..\parsers\typespec.c" />
     <ClCompile Include="..\parsers\v.c" />
     <ClCompile Include="..\parsers\vera.c" />
     <ClCompile Include="..\parsers\verilog.c" />
diff --git a/win32/ctags_vs2013.vcxproj.filters b/win32/ctags_vs2013.vcxproj.filters
index a952353fb8..35ff3fafac 100644
--- a/win32/ctags_vs2013.vcxproj.filters
+++ b/win32/ctags_vs2013.vcxproj.filters
@@ -657,6 +657,9 @@
     <ClCompile Include="..\parsers\typescript.c">
       <Filter>Source Files\parsers</Filter>
     </ClCompile>
+    <ClCompile Include="..\parsers\typespec.c">
+      <Filter>Source Files\parsers</Filter>
+    </ClCompile>
     <ClCompile Include="..\parsers\v.c">
       <Filter>Source Files\parsers</Filter>
     </ClCompile>


Reviews:
user: login: masatake
body: Well written. Thank you.
Could you add the name of the new parser to docs/news/HEAD.rst?

I think we can simplify the code with "cork API", 
See https://docs.ctags.io/en/latest/internal.html#cork-api

I will show you how to apply the CORK API to your parser in the next comment.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-08T16:11:58Z
user: login: masatake
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T16:14:17Z
user: login: masatake
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T16:14:36Z
user: login: masatake
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T16:15:19Z
user: login: masatake
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T16:16:20Z
user: login: masatake
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T16:18:57Z
user: login: iaalm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T12:49:17Z
user: login: iaalm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T12:49:58Z
user: login: masatake
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-15T19:52:06Z
user: login: masatake
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T19:55:45Z
user: login: iaalm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T05:35:34Z
user: login: iaalm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T05:39:09Z
user: login: masatake
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-25T23:28:12Z
user: login: masatake
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-25T23:37:29Z
user: login: masatake
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-25T23:43:17Z
user: login: masatake
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-25T23:47:30Z
user: login: iaalm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T03:37:08Z

Review comments:
diff_hunk: @@ -0,0 +1,749 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enums" },
path: parsers/typespec.c
user: login: masatake
body: Use "enumeration names" instead of "enums" like C parser?

```
$ ctags --list-kinds-full=C | grep enum
e       enumerator yes     no      0      C      enumerators (values inside an enumeration)
g       enum       yes     no      0      C      enumeration names
```
created_at: 2025-05-08T15:31:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,749 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enums" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_EXTENDS,
+	TOKEN_IS,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	vString *		scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+	int				parentKind;  /* KIND_GHOST_INDEX if none */
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= vStringNew ();
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+	token->parentKind	= KIND_GHOST_INDEX;
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	vStringDelete (token->scope);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src,
+					   bool scope)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+	dest->parentKind = src->parentKind;
+	if (scope)
+		vStringCopy (dest->scope, src->scope);
+}
+
+static void addToScope (tokenInfo *const token, const vString *const extra)
+{
+	if (vStringLength (token->scope) > 0)
+		vStringCatS (token->scope, SCOPE_SEPARATOR);
+	vStringCatS (token->scope, vStringValue (extra));
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+		if (c == '\r')
path: parsers/typespec.c
user: login: masatake
body: You can simplify the code for handling line breakers.
See readLine() in main/read.c. It converts `\r\n` to `\n`.
getcFromInputFile() calls readLine().

So, just looking for `\n` is enough for skipping a comment. 
created_at: 2025-05-08T15:43:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,749 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enums" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_EXTENDS,
+	TOKEN_IS,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	vString *		scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+	int				parentKind;  /* KIND_GHOST_INDEX if none */
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= vStringNew ();
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+	token->parentKind	= KIND_GHOST_INDEX;
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	vStringDelete (token->scope);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src,
+					   bool scope)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+	dest->parentKind = src->parentKind;
+	if (scope)
+		vStringCopy (dest->scope, src->scope);
+}
+
+static void addToScope (tokenInfo *const token, const vString *const extra)
+{
+	if (vStringLength (token->scope) > 0)
+		vStringCatS (token->scope, SCOPE_SEPARATOR);
+	vStringCatS (token->scope, vStringValue (extra));
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+		if (c == '\r')
+		{
+			int next = getcFromInputFile ();
+			if (next != '\n')
+				ungetcToInputFile (next);
+			else
+				c = next;
+		}
+	} while (c != EOF && c != '\n' && c != '\r');
+	return c;
+}
+
+static int skipMultiComment (void)
+{
+	int c = getcFromInputFile ();
+	int next = getcFromInputFile ();
+	
+	while (c != EOF && !(c == '*' && next == '/'))
+	{
+		c = next;
+		next = getcFromInputFile ();
+	}
+	
+	return getcFromInputFile ();
+}
+
+static void parseString (vString *const string, const int delimiter)
+{
+	while (true)
+	{
+		int c = getcFromInputFile ();
+
+		if (c == '\\' && (c = getcFromInputFile ()) != EOF)
+			vStringPut (string, c);
+		else if (c == EOF || c == delimiter)
+			break;
+		else
+			vStringPut (string, c);
+	}
+}
+
+static void parseIdentifier (vString *const string, const int firstChar)
+{
+	int c = firstChar;
+	do
+	{
+		vStringPut (string, c);
+		c = getcFromInputFile ();
+	} while (isIdentChar (c));
+	ungetcToInputFile (c);
+}
+
+static void readToken (tokenInfo *const token)
+{
+	int c;
+
+	token->type		= TOKEN_UNDEFINED;
+	vStringClear (token->string);
+
+getNextChar:
+
+	c = getcFromInputFile ();
+	c = skipWhitespace (c);
+
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	switch (c)
+	{
+		case EOF: token->type = TOKEN_EOF;					break;
+		case '(': token->type = TOKEN_OPEN_PAREN;			break;
+		case ')': token->type = TOKEN_CLOSE_PAREN;			break;
+		case ';': token->type = TOKEN_SEMICOLON;			break;
+		case ',': token->type = TOKEN_COMMA;				break;
+		case '.': 
+		{
+			/* Handle ... (spread) operator */
+			int c1 = getcFromInputFile ();
+			int c2 = getcFromInputFile ();
+			if (c1 == '.' && c2 == '.')
+			{
+				token->type = TOKEN_SPREAD;
+			}
+			else
+			{
+				ungetcToInputFile (c2);
+				ungetcToInputFile (c1);
+				token->type = TOKEN_PERIOD;
+			}
+			break;
+		}
+		case ':': token->type = TOKEN_COLON;				break;
+		case '{': token->type = TOKEN_OPEN_CURLY;			break;
+		case '}': token->type = TOKEN_CLOSE_CURLY;			break;
+		case '<': token->type = TOKEN_OPEN_ANGLE;			break;
+		case '>': token->type = TOKEN_CLOSE_ANGLE;			break;
+		case '=': token->type = TOKEN_EQUAL_SIGN;			break;
+		case '[': token->type = TOKEN_OPEN_SQUARE;			break;
+		case ']': token->type = TOKEN_CLOSE_SQUARE;			break;
+
+		case '\'':
+		case '"':
+			token->type = TOKEN_STRING;
+			parseString (token->string, c);
+			break;
+
+		case '/':
+		{
+			int d = getcFromInputFile ();
+			if (d == '/')  /* single-line comment */
+			{
+				skipSingleComment ();
+				goto getNextChar;
+			}
+			else if (d == '*')  /* multi-line comment */
+			{
+				skipMultiComment ();
+				goto getNextChar;
+			}
+			else
+			{
+				ungetcToInputFile (d);
+				token->type = TOKEN_UNDEFINED;
+			}
+			break;
+		}
+
+		case '@':  /* decorator */
+			token->type = TOKEN_DECORATOR;
+			c = getcFromInputFile ();
+			parseIdentifier (token->string, c);
+			break;
+
+		default:
+			if (! isIdentChar (c))
+				token->type = TOKEN_UNDEFINED;
+			else
+			{
+				parseIdentifier (token->string, c);
+				
+				/* Check for special keywords */
+				if (strcmp (vStringValue (token->string), "extends") == 0)
+					token->type = TOKEN_EXTENDS;
path: parsers/typespec.c
user: login: masatake
body: Could you add comments on why (or how) "extends" and "is" are special?

created_at: 2025-05-08T15:52:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,749 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enums" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_EXTENDS,
+	TOKEN_IS,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	vString *		scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+	int				parentKind;  /* KIND_GHOST_INDEX if none */
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= vStringNew ();
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+	token->parentKind	= KIND_GHOST_INDEX;
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	vStringDelete (token->scope);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src,
+					   bool scope)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+	dest->parentKind = src->parentKind;
+	if (scope)
+		vStringCopy (dest->scope, src->scope);
+}
+
+static void addToScope (tokenInfo *const token, const vString *const extra)
+{
+	if (vStringLength (token->scope) > 0)
+		vStringCatS (token->scope, SCOPE_SEPARATOR);
+	vStringCatS (token->scope, vStringValue (extra));
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+		if (c == '\r')
+		{
+			int next = getcFromInputFile ();
+			if (next != '\n')
+				ungetcToInputFile (next);
+			else
+				c = next;
+		}
+	} while (c != EOF && c != '\n' && c != '\r');
+	return c;
+}
+
+static int skipMultiComment (void)
+{
+	int c = getcFromInputFile ();
+	int next = getcFromInputFile ();
+	
+	while (c != EOF && !(c == '*' && next == '/'))
+	{
+		c = next;
+		next = getcFromInputFile ();
+	}
+	
+	return getcFromInputFile ();
+}
+
+static void parseString (vString *const string, const int delimiter)
+{
+	while (true)
+	{
+		int c = getcFromInputFile ();
+
+		if (c == '\\' && (c = getcFromInputFile ()) != EOF)
+			vStringPut (string, c);
+		else if (c == EOF || c == delimiter)
+			break;
+		else
+			vStringPut (string, c);
+	}
+}
+
+static void parseIdentifier (vString *const string, const int firstChar)
+{
+	int c = firstChar;
+	do
+	{
+		vStringPut (string, c);
+		c = getcFromInputFile ();
+	} while (isIdentChar (c));
+	ungetcToInputFile (c);
+}
+
+static void readToken (tokenInfo *const token)
+{
+	int c;
+
+	token->type		= TOKEN_UNDEFINED;
+	vStringClear (token->string);
+
+getNextChar:
+
+	c = getcFromInputFile ();
+	c = skipWhitespace (c);
+
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	switch (c)
+	{
+		case EOF: token->type = TOKEN_EOF;					break;
+		case '(': token->type = TOKEN_OPEN_PAREN;			break;
+		case ')': token->type = TOKEN_CLOSE_PAREN;			break;
+		case ';': token->type = TOKEN_SEMICOLON;			break;
+		case ',': token->type = TOKEN_COMMA;				break;
+		case '.': 
+		{
+			/* Handle ... (spread) operator */
+			int c1 = getcFromInputFile ();
+			int c2 = getcFromInputFile ();
+			if (c1 == '.' && c2 == '.')
+			{
+				token->type = TOKEN_SPREAD;
+			}
+			else
+			{
+				ungetcToInputFile (c2);
+				ungetcToInputFile (c1);
+				token->type = TOKEN_PERIOD;
+			}
+			break;
+		}
+		case ':': token->type = TOKEN_COLON;				break;
+		case '{': token->type = TOKEN_OPEN_CURLY;			break;
+		case '}': token->type = TOKEN_CLOSE_CURLY;			break;
+		case '<': token->type = TOKEN_OPEN_ANGLE;			break;
+		case '>': token->type = TOKEN_CLOSE_ANGLE;			break;
+		case '=': token->type = TOKEN_EQUAL_SIGN;			break;
+		case '[': token->type = TOKEN_OPEN_SQUARE;			break;
+		case ']': token->type = TOKEN_CLOSE_SQUARE;			break;
+
+		case '\'':
+		case '"':
+			token->type = TOKEN_STRING;
+			parseString (token->string, c);
+			break;
+
+		case '/':
+		{
+			int d = getcFromInputFile ();
+			if (d == '/')  /* single-line comment */
+			{
+				skipSingleComment ();
+				goto getNextChar;
+			}
+			else if (d == '*')  /* multi-line comment */
+			{
+				skipMultiComment ();
+				goto getNextChar;
+			}
+			else
+			{
+				ungetcToInputFile (d);
+				token->type = TOKEN_UNDEFINED;
+			}
+			break;
+		}
+
+		case '@':  /* decorator */
+			token->type = TOKEN_DECORATOR;
+			c = getcFromInputFile ();
+			parseIdentifier (token->string, c);
+			break;
+
+		default:
+			if (! isIdentChar (c))
+				token->type = TOKEN_UNDEFINED;
+			else
+			{
+				parseIdentifier (token->string, c);
+				
+				/* Check for special keywords */
+				if (strcmp (vStringValue (token->string), "extends") == 0)
+					token->type = TOKEN_EXTENDS;
+				else if (strcmp (vStringValue (token->string), "is") == 0)
+					token->type = TOKEN_IS;
+				else {
+					token->keyword = lookupCaseKeyword (
+						vStringValue (token->string), getInputLanguage ());
+					if (token->keyword == KEYWORD_NONE)
+						token->type = TOKEN_IDENTIFIER;
+					else
+						token->type = TOKEN_KEYWORD;
+				}
+			}
+			break;
+	}
+}
+
+static void initTypeSpecEntry (tagEntryInfo *const e, const tokenInfo *const token,
+							  const typeSpecKind kind)
+{
+	initTagEntry (e, vStringValue (token->string), kind);
+
+	e->lineNumber	= token->lineNumber;
+	e->filePosition	= token->filePosition;
+
+	if (vStringLength (token->scope) > 0)
path: parsers/typespec.c
user: login: masatake
body: I prefer `!vStringIsEmpty (token->scope)`

created_at: 2025-05-08T15:56:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,749 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enums" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_EXTENDS,
+	TOKEN_IS,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	vString *		scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+	int				parentKind;  /* KIND_GHOST_INDEX if none */
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= vStringNew ();
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+	token->parentKind	= KIND_GHOST_INDEX;
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	vStringDelete (token->scope);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src,
+					   bool scope)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+	dest->parentKind = src->parentKind;
+	if (scope)
+		vStringCopy (dest->scope, src->scope);
+}
+
+static void addToScope (tokenInfo *const token, const vString *const extra)
+{
+	if (vStringLength (token->scope) > 0)
+		vStringCatS (token->scope, SCOPE_SEPARATOR);
+	vStringCatS (token->scope, vStringValue (extra));
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+		if (c == '\r')
+		{
+			int next = getcFromInputFile ();
+			if (next != '\n')
+				ungetcToInputFile (next);
+			else
+				c = next;
+		}
+	} while (c != EOF && c != '\n' && c != '\r');
+	return c;
+}
+
+static int skipMultiComment (void)
+{
+	int c = getcFromInputFile ();
+	int next = getcFromInputFile ();
+	
path: parsers/typespec.c
user: login: masatake
body: Remove the tab char at the beginning of line.
created_at: 2025-05-08T16:14:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,749 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enums" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_EXTENDS,
+	TOKEN_IS,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	vString *		scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+	int				parentKind;  /* KIND_GHOST_INDEX if none */
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= vStringNew ();
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+	token->parentKind	= KIND_GHOST_INDEX;
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	vStringDelete (token->scope);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src,
+					   bool scope)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+	dest->parentKind = src->parentKind;
+	if (scope)
+		vStringCopy (dest->scope, src->scope);
+}
+
+static void addToScope (tokenInfo *const token, const vString *const extra)
+{
+	if (vStringLength (token->scope) > 0)
+		vStringCatS (token->scope, SCOPE_SEPARATOR);
+	vStringCatS (token->scope, vStringValue (extra));
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+		if (c == '\r')
+		{
+			int next = getcFromInputFile ();
+			if (next != '\n')
+				ungetcToInputFile (next);
+			else
+				c = next;
+		}
+	} while (c != EOF && c != '\n' && c != '\r');
+	return c;
+}
+
+static int skipMultiComment (void)
+{
+	int c = getcFromInputFile ();
+	int next = getcFromInputFile ();
+	
+	while (c != EOF && !(c == '*' && next == '/'))
+	{
+		c = next;
+		next = getcFromInputFile ();
+	}
+	
path: parsers/typespec.c
user: login: masatake
body: Remove the tab char at the beginning of line.
created_at: 2025-05-08T16:14:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,749 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enums" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_EXTENDS,
+	TOKEN_IS,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	vString *		scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+	int				parentKind;  /* KIND_GHOST_INDEX if none */
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= vStringNew ();
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+	token->parentKind	= KIND_GHOST_INDEX;
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	vStringDelete (token->scope);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src,
+					   bool scope)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+	dest->parentKind = src->parentKind;
+	if (scope)
+		vStringCopy (dest->scope, src->scope);
+}
+
+static void addToScope (tokenInfo *const token, const vString *const extra)
+{
+	if (vStringLength (token->scope) > 0)
+		vStringCatS (token->scope, SCOPE_SEPARATOR);
+	vStringCatS (token->scope, vStringValue (extra));
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+		if (c == '\r')
+		{
+			int next = getcFromInputFile ();
+			if (next != '\n')
+				ungetcToInputFile (next);
+			else
+				c = next;
+		}
+	} while (c != EOF && c != '\n' && c != '\r');
+	return c;
+}
+
+static int skipMultiComment (void)
+{
+	int c = getcFromInputFile ();
+	int next = getcFromInputFile ();
+	
+	while (c != EOF && !(c == '*' && next == '/'))
+	{
+		c = next;
+		next = getcFromInputFile ();
+	}
+	
+	return getcFromInputFile ();
+}
+
+static void parseString (vString *const string, const int delimiter)
+{
+	while (true)
+	{
+		int c = getcFromInputFile ();
+
+		if (c == '\\' && (c = getcFromInputFile ()) != EOF)
+			vStringPut (string, c);
+		else if (c == EOF || c == delimiter)
+			break;
+		else
+			vStringPut (string, c);
+	}
+}
+
+static void parseIdentifier (vString *const string, const int firstChar)
+{
+	int c = firstChar;
+	do
+	{
+		vStringPut (string, c);
+		c = getcFromInputFile ();
+	} while (isIdentChar (c));
+	ungetcToInputFile (c);
+}
+
+static void readToken (tokenInfo *const token)
+{
+	int c;
+
+	token->type		= TOKEN_UNDEFINED;
+	vStringClear (token->string);
+
+getNextChar:
+
+	c = getcFromInputFile ();
+	c = skipWhitespace (c);
+
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	switch (c)
+	{
+		case EOF: token->type = TOKEN_EOF;					break;
+		case '(': token->type = TOKEN_OPEN_PAREN;			break;
+		case ')': token->type = TOKEN_CLOSE_PAREN;			break;
+		case ';': token->type = TOKEN_SEMICOLON;			break;
+		case ',': token->type = TOKEN_COMMA;				break;
+		case '.': 
path: parsers/typespec.c
user: login: masatake
body: Remove the space char at the end of line.
created_at: 2025-05-08T16:15:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,749 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enums" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_EXTENDS,
+	TOKEN_IS,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	vString *		scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+	int				parentKind;  /* KIND_GHOST_INDEX if none */
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= vStringNew ();
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+	token->parentKind	= KIND_GHOST_INDEX;
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	vStringDelete (token->scope);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src,
+					   bool scope)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+	dest->parentKind = src->parentKind;
+	if (scope)
+		vStringCopy (dest->scope, src->scope);
+}
+
+static void addToScope (tokenInfo *const token, const vString *const extra)
+{
+	if (vStringLength (token->scope) > 0)
+		vStringCatS (token->scope, SCOPE_SEPARATOR);
+	vStringCatS (token->scope, vStringValue (extra));
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+		if (c == '\r')
+		{
+			int next = getcFromInputFile ();
+			if (next != '\n')
+				ungetcToInputFile (next);
+			else
+				c = next;
+		}
+	} while (c != EOF && c != '\n' && c != '\r');
+	return c;
+}
+
+static int skipMultiComment (void)
+{
+	int c = getcFromInputFile ();
+	int next = getcFromInputFile ();
+	
+	while (c != EOF && !(c == '*' && next == '/'))
+	{
+		c = next;
+		next = getcFromInputFile ();
+	}
+	
+	return getcFromInputFile ();
+}
+
+static void parseString (vString *const string, const int delimiter)
+{
+	while (true)
+	{
+		int c = getcFromInputFile ();
+
+		if (c == '\\' && (c = getcFromInputFile ()) != EOF)
+			vStringPut (string, c);
+		else if (c == EOF || c == delimiter)
+			break;
+		else
+			vStringPut (string, c);
+	}
+}
+
+static void parseIdentifier (vString *const string, const int firstChar)
+{
+	int c = firstChar;
+	do
+	{
+		vStringPut (string, c);
+		c = getcFromInputFile ();
+	} while (isIdentChar (c));
+	ungetcToInputFile (c);
+}
+
+static void readToken (tokenInfo *const token)
+{
+	int c;
+
+	token->type		= TOKEN_UNDEFINED;
+	vStringClear (token->string);
+
+getNextChar:
+
+	c = getcFromInputFile ();
+	c = skipWhitespace (c);
+
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	switch (c)
+	{
+		case EOF: token->type = TOKEN_EOF;					break;
+		case '(': token->type = TOKEN_OPEN_PAREN;			break;
+		case ')': token->type = TOKEN_CLOSE_PAREN;			break;
+		case ';': token->type = TOKEN_SEMICOLON;			break;
+		case ',': token->type = TOKEN_COMMA;				break;
+		case '.': 
+		{
+			/* Handle ... (spread) operator */
+			int c1 = getcFromInputFile ();
+			int c2 = getcFromInputFile ();
+			if (c1 == '.' && c2 == '.')
+			{
+				token->type = TOKEN_SPREAD;
+			}
+			else
+			{
+				ungetcToInputFile (c2);
+				ungetcToInputFile (c1);
+				token->type = TOKEN_PERIOD;
+			}
+			break;
+		}
+		case ':': token->type = TOKEN_COLON;				break;
+		case '{': token->type = TOKEN_OPEN_CURLY;			break;
+		case '}': token->type = TOKEN_CLOSE_CURLY;			break;
+		case '<': token->type = TOKEN_OPEN_ANGLE;			break;
+		case '>': token->type = TOKEN_CLOSE_ANGLE;			break;
+		case '=': token->type = TOKEN_EQUAL_SIGN;			break;
+		case '[': token->type = TOKEN_OPEN_SQUARE;			break;
+		case ']': token->type = TOKEN_CLOSE_SQUARE;			break;
+
+		case '\'':
+		case '"':
+			token->type = TOKEN_STRING;
+			parseString (token->string, c);
+			break;
+
+		case '/':
+		{
+			int d = getcFromInputFile ();
+			if (d == '/')  /* single-line comment */
+			{
+				skipSingleComment ();
+				goto getNextChar;
+			}
+			else if (d == '*')  /* multi-line comment */
+			{
+				skipMultiComment ();
+				goto getNextChar;
+			}
+			else
+			{
+				ungetcToInputFile (d);
+				token->type = TOKEN_UNDEFINED;
+			}
+			break;
+		}
+
+		case '@':  /* decorator */
+			token->type = TOKEN_DECORATOR;
+			c = getcFromInputFile ();
+			parseIdentifier (token->string, c);
+			break;
+
+		default:
+			if (! isIdentChar (c))
+				token->type = TOKEN_UNDEFINED;
+			else
+			{
+				parseIdentifier (token->string, c);
+				
path: parsers/typespec.c
user: login: masatake
body: Remove tab chars at the beginning of lines.
created_at: 2025-05-08T16:16:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,749 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enums" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_EXTENDS,
+	TOKEN_IS,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	vString *		scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+	int				parentKind;  /* KIND_GHOST_INDEX if none */
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= vStringNew ();
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+	token->parentKind	= KIND_GHOST_INDEX;
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	vStringDelete (token->scope);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src,
+					   bool scope)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+	dest->parentKind = src->parentKind;
+	if (scope)
+		vStringCopy (dest->scope, src->scope);
+}
+
+static void addToScope (tokenInfo *const token, const vString *const extra)
+{
+	if (vStringLength (token->scope) > 0)
+		vStringCatS (token->scope, SCOPE_SEPARATOR);
+	vStringCatS (token->scope, vStringValue (extra));
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+		if (c == '\r')
+		{
+			int next = getcFromInputFile ();
+			if (next != '\n')
+				ungetcToInputFile (next);
+			else
+				c = next;
+		}
+	} while (c != EOF && c != '\n' && c != '\r');
+	return c;
+}
+
+static int skipMultiComment (void)
+{
+	int c = getcFromInputFile ();
+	int next = getcFromInputFile ();
+	
+	while (c != EOF && !(c == '*' && next == '/'))
+	{
+		c = next;
+		next = getcFromInputFile ();
+	}
+	
+	return getcFromInputFile ();
+}
+
+static void parseString (vString *const string, const int delimiter)
+{
+	while (true)
+	{
+		int c = getcFromInputFile ();
+
+		if (c == '\\' && (c = getcFromInputFile ()) != EOF)
+			vStringPut (string, c);
+		else if (c == EOF || c == delimiter)
+			break;
+		else
+			vStringPut (string, c);
+	}
+}
+
+static void parseIdentifier (vString *const string, const int firstChar)
+{
+	int c = firstChar;
+	do
+	{
+		vStringPut (string, c);
+		c = getcFromInputFile ();
+	} while (isIdentChar (c));
+	ungetcToInputFile (c);
+}
+
+static void readToken (tokenInfo *const token)
+{
+	int c;
+
+	token->type		= TOKEN_UNDEFINED;
+	vStringClear (token->string);
+
+getNextChar:
+
+	c = getcFromInputFile ();
+	c = skipWhitespace (c);
+
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	switch (c)
+	{
+		case EOF: token->type = TOKEN_EOF;					break;
+		case '(': token->type = TOKEN_OPEN_PAREN;			break;
+		case ')': token->type = TOKEN_CLOSE_PAREN;			break;
+		case ';': token->type = TOKEN_SEMICOLON;			break;
+		case ',': token->type = TOKEN_COMMA;				break;
+		case '.': 
+		{
+			/* Handle ... (spread) operator */
+			int c1 = getcFromInputFile ();
+			int c2 = getcFromInputFile ();
+			if (c1 == '.' && c2 == '.')
+			{
+				token->type = TOKEN_SPREAD;
+			}
+			else
+			{
+				ungetcToInputFile (c2);
+				ungetcToInputFile (c1);
+				token->type = TOKEN_PERIOD;
+			}
+			break;
+		}
+		case ':': token->type = TOKEN_COLON;				break;
+		case '{': token->type = TOKEN_OPEN_CURLY;			break;
+		case '}': token->type = TOKEN_CLOSE_CURLY;			break;
+		case '<': token->type = TOKEN_OPEN_ANGLE;			break;
+		case '>': token->type = TOKEN_CLOSE_ANGLE;			break;
+		case '=': token->type = TOKEN_EQUAL_SIGN;			break;
+		case '[': token->type = TOKEN_OPEN_SQUARE;			break;
+		case ']': token->type = TOKEN_CLOSE_SQUARE;			break;
+
+		case '\'':
+		case '"':
+			token->type = TOKEN_STRING;
+			parseString (token->string, c);
+			break;
+
+		case '/':
+		{
+			int d = getcFromInputFile ();
+			if (d == '/')  /* single-line comment */
+			{
+				skipSingleComment ();
+				goto getNextChar;
+			}
+			else if (d == '*')  /* multi-line comment */
+			{
+				skipMultiComment ();
+				goto getNextChar;
+			}
+			else
+			{
+				ungetcToInputFile (d);
+				token->type = TOKEN_UNDEFINED;
+			}
+			break;
+		}
+
+		case '@':  /* decorator */
+			token->type = TOKEN_DECORATOR;
+			c = getcFromInputFile ();
+			parseIdentifier (token->string, c);
+			break;
+
+		default:
+			if (! isIdentChar (c))
+				token->type = TOKEN_UNDEFINED;
+			else
+			{
+				parseIdentifier (token->string, c);
+				
path: parsers/typespec.c
user: login: masatake
body: There are many unnecessary white spaces and tabs. I cannot remember how to configure, but git show can visualize them.

![image](https://github.com/user-attachments/assets/710c4988-9311-44b7-8c4f-93dab8e8353a)

created_at: 2025-05-08T16:18:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,749 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enums" },
path: parsers/typespec.c
user: login: iaalm
body: Sure, updated
created_at: 2025-05-15T12:49:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,749 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enums" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_EXTENDS,
+	TOKEN_IS,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	vString *		scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+	int				parentKind;  /* KIND_GHOST_INDEX if none */
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= vStringNew ();
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+	token->parentKind	= KIND_GHOST_INDEX;
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	vStringDelete (token->scope);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src,
+					   bool scope)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+	dest->parentKind = src->parentKind;
+	if (scope)
+		vStringCopy (dest->scope, src->scope);
+}
+
+static void addToScope (tokenInfo *const token, const vString *const extra)
+{
+	if (vStringLength (token->scope) > 0)
+		vStringCatS (token->scope, SCOPE_SEPARATOR);
+	vStringCatS (token->scope, vStringValue (extra));
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+		if (c == '\r')
+		{
+			int next = getcFromInputFile ();
+			if (next != '\n')
+				ungetcToInputFile (next);
+			else
+				c = next;
+		}
+	} while (c != EOF && c != '\n' && c != '\r');
+	return c;
+}
+
+static int skipMultiComment (void)
+{
+	int c = getcFromInputFile ();
+	int next = getcFromInputFile ();
+	
+	while (c != EOF && !(c == '*' && next == '/'))
+	{
+		c = next;
+		next = getcFromInputFile ();
+	}
+	
+	return getcFromInputFile ();
+}
+
+static void parseString (vString *const string, const int delimiter)
+{
+	while (true)
+	{
+		int c = getcFromInputFile ();
+
+		if (c == '\\' && (c = getcFromInputFile ()) != EOF)
+			vStringPut (string, c);
+		else if (c == EOF || c == delimiter)
+			break;
+		else
+			vStringPut (string, c);
+	}
+}
+
+static void parseIdentifier (vString *const string, const int firstChar)
+{
+	int c = firstChar;
+	do
+	{
+		vStringPut (string, c);
+		c = getcFromInputFile ();
+	} while (isIdentChar (c));
+	ungetcToInputFile (c);
+}
+
+static void readToken (tokenInfo *const token)
+{
+	int c;
+
+	token->type		= TOKEN_UNDEFINED;
+	vStringClear (token->string);
+
+getNextChar:
+
+	c = getcFromInputFile ();
+	c = skipWhitespace (c);
+
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	switch (c)
+	{
+		case EOF: token->type = TOKEN_EOF;					break;
+		case '(': token->type = TOKEN_OPEN_PAREN;			break;
+		case ')': token->type = TOKEN_CLOSE_PAREN;			break;
+		case ';': token->type = TOKEN_SEMICOLON;			break;
+		case ',': token->type = TOKEN_COMMA;				break;
+		case '.': 
+		{
+			/* Handle ... (spread) operator */
+			int c1 = getcFromInputFile ();
+			int c2 = getcFromInputFile ();
+			if (c1 == '.' && c2 == '.')
+			{
+				token->type = TOKEN_SPREAD;
+			}
+			else
+			{
+				ungetcToInputFile (c2);
+				ungetcToInputFile (c1);
+				token->type = TOKEN_PERIOD;
+			}
+			break;
+		}
+		case ':': token->type = TOKEN_COLON;				break;
+		case '{': token->type = TOKEN_OPEN_CURLY;			break;
+		case '}': token->type = TOKEN_CLOSE_CURLY;			break;
+		case '<': token->type = TOKEN_OPEN_ANGLE;			break;
+		case '>': token->type = TOKEN_CLOSE_ANGLE;			break;
+		case '=': token->type = TOKEN_EQUAL_SIGN;			break;
+		case '[': token->type = TOKEN_OPEN_SQUARE;			break;
+		case ']': token->type = TOKEN_CLOSE_SQUARE;			break;
+
+		case '\'':
+		case '"':
+			token->type = TOKEN_STRING;
+			parseString (token->string, c);
+			break;
+
+		case '/':
+		{
+			int d = getcFromInputFile ();
+			if (d == '/')  /* single-line comment */
+			{
+				skipSingleComment ();
+				goto getNextChar;
+			}
+			else if (d == '*')  /* multi-line comment */
+			{
+				skipMultiComment ();
+				goto getNextChar;
+			}
+			else
+			{
+				ungetcToInputFile (d);
+				token->type = TOKEN_UNDEFINED;
+			}
+			break;
+		}
+
+		case '@':  /* decorator */
+			token->type = TOKEN_DECORATOR;
+			c = getcFromInputFile ();
+			parseIdentifier (token->string, c);
+			break;
+
+		default:
+			if (! isIdentChar (c))
+				token->type = TOKEN_UNDEFINED;
+			else
+			{
+				parseIdentifier (token->string, c);
+				
path: parsers/typespec.c
user: login: iaalm
body: Thanks, I have updated.
created_at: 2025-05-15T12:49:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+import "@typespec/http";
+import "./common.tsp";
+
+using TypeSpec;
+using TypeSpec.Rest;
+
+@armProviderNamespace
+@service(#{ title: "Name.Space" })
+@versioned(Versions)
+@armCommonTypesVersion(CommonTypes.Versions.v3)
+namespace Name.Space;
+
+@doc("API Versions")
+enum Versions {
path: Units/parser-typespec.r/simple-typespec.d/input.tsp
user: login: masatake
body: Shouldn't we fill the scope: field of  `Versions` with `Name.Space`?
(This comment applies to the other extractions.)

* No, TypeSpec is not such a language,
* Yes, "I'm interested in implementing it in this pull request", or
* Yes, but someone will fix it in the future.
 
 If you're interested in implementing it in this pull request, talk to me. I have some hints.
 Of course, "Yes, but someone ..." is acceptable.
 
 ---
 
 Let's call the above three items _NYY-choice_.
created_at: 2025-05-15T18:56:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,83 @@
+import "@typespec/http";
+import "./common.tsp";
+
+using TypeSpec;
+using TypeSpec.Rest;
+
+@armProviderNamespace
+@service(#{ title: "Name.Space" })
+@versioned(Versions)
+@armCommonTypesVersion(CommonTypes.Versions.v3)
+namespace Name.Space;
+
+@doc("API Versions")
+enum Versions {
+  @doc("May 01, 2024 Preview API Version")
+  v2024_05_01_preview: "2024-05-01-preview",
path: Units/parser-typespec.r/simple-typespec.d/input.tsp
user: login: masatake
body: Shouldn't we extract `v2024_05_01_preview` ?

NYY-choice.
created_at: 2025-05-15T19:01:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,83 @@
+import "@typespec/http";
+import "./common.tsp";
+
+using TypeSpec;
+using TypeSpec.Rest;
path: Units/parser-typespec.r/simple-typespec.d/input.tsp
user: login: masatake
body: I'm interested in extracting `import`ed items and `using`ed items.
I will work on them after merging this pull request.

https://github.com/universal-ctags/ctags/issues/3906
https://github.com/universal-ctags/ctags/issues/2428
created_at: 2025-05-15T19:07:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,83 @@
+import "@typespec/http";
+import "./common.tsp";
+
+using TypeSpec;
+using TypeSpec.Rest;
+
+@armProviderNamespace
+@service(#{ title: "Name.Space" })
+@versioned(Versions)
+@armCommonTypesVersion(CommonTypes.Versions.v3)
+namespace Name.Space;
+
+@doc("API Versions")
+enum Versions {
+  @doc("May 01, 2024 Preview API Version")
+  v2024_05_01_preview: "2024-05-01-preview",
+}
+
+@summary("Example operation summary for demonstration purposes.")
+@doc("""
+  Example
+  multi-line
+  documentation.
+  """)
+@returnsDoc("Example return documentation")
+@post
+op OpA<T extends TypeSpec.Reflection.Model> is OpB<
+  T & ModelA,
+  ModelB<ModelA[]>
+>;
+
+interface InterfaceA extends InterfaceB {
path: Units/parser-typespec.r/simple-typespec.d/input.tsp
user: login: masatake
body: We can fill `inherits:` field of `interfaceA:` with `InterfaceB`.
NYY-choice.


Filling such a field makes your work applicable to broader use cases.

Here is an example of C++.

```console
$ cat /tmp/foo.cc 
class A: B {
	int x;
};
$ ./ctags -o - /tmp/foo.cc
A	/tmp/foo.cc	/^class A: B {$/;"	c	file:
x	/tmp/foo.cc	/^	int x;$/;"	m	class:A	typeref:typename:int	file:
$ ./ctags --fields=+i -o - /tmp/foo.cc
A	/tmp/foo.cc	/^class A: B {$/;"	c	file:	inherits:B
x	/tmp/foo.cc	/^	int x;$/;"	m	class:A	typeref:typename:int	file:
$ ./ctags --fields=+i -o foo.tags /tmp/foo.cc
$ ~/bin/querytags --tag-file foo.tags class-hierarchy  A
  A
   B
```

created_at: 2025-05-15T19:16:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,708 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enumeration names" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
path: parsers/typespec.c
user: login: masatake
body: My reviewing log: These are good. They align with the kinds of TypeScript.
created_at: 2025-05-15T19:35:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,708 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enumeration names" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import,
+	KEYWORD_is,
+	KEYWORD_extends
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import },
+	{ "is",         KEYWORD_is },
+	{ "extends",    KEYWORD_extends }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	int				scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= CORK_NIL;
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src,
+					   bool copyScope)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+	if (copyScope)
path: parsers/typespec.c
user: login: masatake
body: It seems that no caller passes `true` as `copyScope`. So you can remove the branch related to copyScope. Another choice is to keep `dest->scope = src->scope;` without the condition.
created_at: 2025-05-15T19:39:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,83 @@
+import "@typespec/http";
+import "./common.tsp";
+
+using TypeSpec;
+using TypeSpec.Rest;
+
+@armProviderNamespace
+@service(#{ title: "Name.Space" })
+@versioned(Versions)
+@armCommonTypesVersion(CommonTypes.Versions.v3)
+namespace Name.Space;
+
+@doc("API Versions")
+enum Versions {
+  @doc("May 01, 2024 Preview API Version")
+  v2024_05_01_preview: "2024-05-01-preview",
+}
+
+@summary("Example operation summary for demonstration purposes.")
+@doc("""
+  Example
+  multi-line
+  documentation.
+  """)
+@returnsDoc("Example return documentation")
+@post
+op OpA<T extends TypeSpec.Reflection.Model> is OpB<
+  T & ModelA,
+  ModelB<ModelA[]>
+>;
+
+interface InterfaceA extends InterfaceB {
+  opA is OpA<{
+    @doc("The format of the HTTP payload.")
+    @header
+    contentType: "application/json";
+
+    @clientName("uri", "csharp")
+    @doc("Example URL parameter.")
+    opB: url;
+  }>;
+
+  /**
+   * Example operation description
+   */
+  opB is OpB<
+    ModelA,
+    {
+      ...ModelB
+    }
+  >;
+}
+
+@doc("Example union type description.")
+union UnionA {
+  string,
+
+  @doc("Example option A description with some technical details that would be typical for this kind of documentation.")
+  "unionA",
+
+  @doc("Example option B description with additional technical context that would be common in API documentation.")
+  "unionB",
+}
+
+@doc("Example model description.")
+model ModelA {
+  @doc("Example property description.")
+  contentA: ModelB;
+
+  @removed(Versions.v1_2)
+  @doc("Example boolean property with typical documentation about its purpose and default behavior.")
+  contentB?: boolean;
+
+  ...ModelX;
+}
+
+
+alias AliasA<
+  TParamA extends ModelX,
+  TParamB,
+  TParamC extends ModelX = {},
+  TError = Error
+> = ModelB<TParamA, TParamB, TParamC, TError>;
path: Units/parser-typespec.r/simple-typespec.d/input.tsp
user: login: masatake
body: Let's increase the testing coverage, though. We don't have a target percentage.

E.g., the code skipping
```
 // comment
```
is not tested at all.

created_at: 2025-05-15T19:44:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,83 @@
+import "@typespec/http";
+import "./common.tsp";
+
+using TypeSpec;
+using TypeSpec.Rest;
+
+@armProviderNamespace
+@service(#{ title: "Name.Space" })
+@versioned(Versions)
+@armCommonTypesVersion(CommonTypes.Versions.v3)
+namespace Name.Space;
+
+@doc("API Versions")
+enum Versions {
+  @doc("May 01, 2024 Preview API Version")
+  v2024_05_01_preview: "2024-05-01-preview",
+}
+
+@summary("Example operation summary for demonstration purposes.")
+@doc("""
+  Example
+  multi-line
+  documentation.
+  """)
+@returnsDoc("Example return documentation")
+@post
+op OpA<T extends TypeSpec.Reflection.Model> is OpB<
+  T & ModelA,
+  ModelB<ModelA[]>
+>;
+
+interface InterfaceA extends InterfaceB {
+  opA is OpA<{
+    @doc("The format of the HTTP payload.")
+    @header
+    contentType: "application/json";
+
+    @clientName("uri", "csharp")
+    @doc("Example URL parameter.")
+    opB: url;
+  }>;
+
+  /**
+   * Example operation description
+   */
+  opB is OpB<
+    ModelA,
+    {
+      ...ModelB
+    }
+  >;
+}
+
+@doc("Example union type description.")
+union UnionA {
+  string,
+
+  @doc("Example option A description with some technical details that would be typical for this kind of documentation.")
+  "unionA",
+
+  @doc("Example option B description with additional technical context that would be common in API documentation.")
+  "unionB",
+}
+
+@doc("Example model description.")
+model ModelA {
+  @doc("Example property description.")
+  contentA: ModelB;
+
+  @removed(Versions.v1_2)
+  @doc("Example boolean property with typical documentation about its purpose and default behavior.")
+  contentB?: boolean;
+
+  ...ModelX;
+}
+
+
+alias AliasA<
+  TParamA extends ModelX,
+  TParamB,
+  TParamC extends ModelX = {},
+  TError = Error
+> = ModelB<TParamA, TParamB, TParamC, TError>;
path: Units/parser-typespec.r/simple-typespec.d/input.tsp
user: login: masatake
body: String literal including `\` is not converted.
created_at: 2025-05-15T19:45:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,708 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enumeration names" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import,
+	KEYWORD_is,
+	KEYWORD_extends
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import },
+	{ "is",         KEYWORD_is },
+	{ "extends",    KEYWORD_extends }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	int				scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= CORK_NIL;
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src,
+					   bool copyScope)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+	if (copyScope)
+		dest->scope = src->scope;
+}
+
+static void setScope (tokenInfo *const token, int scope)
+{
+	token->scope = scope;
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+	} while (c != EOF && c != '\n');
+	return c;
+}
+
+static int skipMultiComment (void)
+{
+	int c = getcFromInputFile ();
+	int next = getcFromInputFile ();
+
+	while (c != EOF && !(c == '*' && next == '/'))
+	{
+		c = next;
+		next = getcFromInputFile ();
+	}
+
+	return getcFromInputFile ();
+}
+
+static void parseString (vString *const string, const int delimiter)
+{
+	while (true)
+	{
+		int c = getcFromInputFile ();
+
+		if (c == '\\' && (c = getcFromInputFile ()) != EOF)
+			vStringPut (string, c);
+		else if (c == EOF || c == delimiter)
+			break;
+		else
+			vStringPut (string, c);
+	}
+}
+
+static void parseIdentifier (vString *const string, const int firstChar)
+{
+	int c = firstChar;
+	do
+	{
+		vStringPut (string, c);
+		c = getcFromInputFile ();
+	} while (isIdentChar (c));
+	ungetcToInputFile (c);
+}
+
+static void readToken (tokenInfo *const token)
+{
+	int c;
+
+	token->type		= TOKEN_UNDEFINED;
+	vStringClear (token->string);
+
+getNextChar:
+
+	c = getcFromInputFile ();
+	c = skipWhitespace (c);
+
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	switch (c)
+	{
+		case EOF: token->type = TOKEN_EOF;					break;
+		case '(': token->type = TOKEN_OPEN_PAREN;			break;
+		case ')': token->type = TOKEN_CLOSE_PAREN;			break;
+		case ';': token->type = TOKEN_SEMICOLON;			break;
+		case ',': token->type = TOKEN_COMMA;				break;
+		case '.':
+		{
+			/* Handle ... (spread) operator */
+			int c1 = getcFromInputFile ();
+			int c2 = getcFromInputFile ();
+			if (c1 == '.' && c2 == '.')
+			{
+				token->type = TOKEN_SPREAD;
+			}
+			else
+			{
+				ungetcToInputFile (c2);
+				ungetcToInputFile (c1);
+				token->type = TOKEN_PERIOD;
+			}
+			break;
+		}
+		case ':': token->type = TOKEN_COLON;				break;
+		case '{': token->type = TOKEN_OPEN_CURLY;			break;
+		case '}': token->type = TOKEN_CLOSE_CURLY;			break;
+		case '<': token->type = TOKEN_OPEN_ANGLE;			break;
+		case '>': token->type = TOKEN_CLOSE_ANGLE;			break;
+		case '=': token->type = TOKEN_EQUAL_SIGN;			break;
+		case '[': token->type = TOKEN_OPEN_SQUARE;			break;
+		case ']': token->type = TOKEN_CLOSE_SQUARE;			break;
+
+		case '\'':
+		case '"':
+			token->type = TOKEN_STRING;
+			parseString (token->string, c);
+			break;
+
+		case '/':
+		{
+			int d = getcFromInputFile ();
+			if (d == '/')  /* single-line comment */
+			{
+				skipSingleComment ();
+				goto getNextChar;
+			}
+			else if (d == '*')  /* multi-line comment */
+			{
+				skipMultiComment ();
+				goto getNextChar;
+			}
+			else
+			{
+				ungetcToInputFile (d);
+				token->type = TOKEN_UNDEFINED;
+			}
+			break;
+		}
+
+		case '@':  /* decorator */
+			token->type = TOKEN_DECORATOR;
+			c = getcFromInputFile ();
+			parseIdentifier (token->string, c);
+			break;
+
+		default:
+			if (! isIdentChar (c))
+				token->type = TOKEN_UNDEFINED;
+			else
+			{
+				parseIdentifier (token->string, c);
+                token->keyword = lookupCaseKeyword (
+                    vStringValue (token->string), getInputLanguage ());
+                if (token->keyword == KEYWORD_NONE)
+                    token->type = TOKEN_IDENTIFIER;
+                else
+                    token->type = TOKEN_KEYWORD;
+			}
+			break;
+	}
+}
+
+static void initTypeSpecEntry (tagEntryInfo *const e, const tokenInfo *const token,
+							  const typeSpecKind kind)
+{
+	initTagEntry (e, vStringValue (token->string), kind);
+
+	e->lineNumber	= token->lineNumber;
+	e->filePosition	= token->filePosition;
+
+	e->extensionFields.scopeIndex = token->scope;
+}
+
+static int makeTypeSpecTag (const tokenInfo *const token, const typeSpecKind kind)
+{
+	tagEntryInfo e;
+	initTypeSpecEntry (&e, token, kind);
+	return makeTagEntry (&e);
+}
+
+static void enterScope (tokenInfo *const parentToken, int scope);
+
+static void skipTypeParameters(tokenInfo *const token)
+{
+	int depth = 1;
+
+	while (depth > 0 && token->type != TOKEN_EOF)
+	{
+		readToken(token);
+
+		if (token->type == TOKEN_OPEN_ANGLE)
+			depth++;
+		else if (token->type == TOKEN_CLOSE_ANGLE)
+			depth--;
+	}
+}
+
+static void skipEntityBody(tokenInfo *const token)
+{
+	int depth = 1;
+
+	while (depth > 0 && token->type != TOKEN_EOF)
+	{
+		readToken(token);
+
+		if (token->type == TOKEN_OPEN_CURLY)
+			depth++;
+		else if (token->type == TOKEN_CLOSE_CURLY)
+			depth--;
+	}
+}
+
+static void skipToSemicolon(tokenInfo *const token)
+{
+	while (token->type != TOKEN_SEMICOLON && token->type != TOKEN_EOF)
+	{
+		readToken(token);
+	}
+}
+
+static void parseNamespace(tokenInfo *const token)
+{
+	vString *name = vStringNew();
+
+	/* Read namespace components */
+	do
+	{
+		readToken(token);
+
+		if (token->type == TOKEN_IDENTIFIER)
+		{
+			if (!vStringIsEmpty(name))
+				vStringCatS(name, SCOPE_SEPARATOR);
+			vStringCat(name, token->string);
+
+			readToken(token);
+		}
+		else
+		{
+			break;
+		}
+	} while (token->type == TOKEN_PERIOD);
+
+	/* Create namespace tag */
+	vStringCopy(token->string, name);
+	int namespaceIndex = makeTypeSpecTag(token, K_NAMESPACE);
+
+	/* Parse namespace body */
+	if (token->type == TOKEN_SEMICOLON)
+	{
+		/* Empty namespace, just a declaration */
+	}
+	else if (token->type == TOKEN_OPEN_CURLY)
+	{
+		enterScope(token, namespaceIndex);
path: parsers/typespec.c
user: login: masatake
body: This branch should be tested.
created_at: 2025-05-15T19:50:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,708 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enumeration names" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import,
+	KEYWORD_is,
+	KEYWORD_extends
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import },
+	{ "is",         KEYWORD_is },
+	{ "extends",    KEYWORD_extends }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	int				scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= CORK_NIL;
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src,
+					   bool copyScope)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+	if (copyScope)
+		dest->scope = src->scope;
+}
+
+static void setScope (tokenInfo *const token, int scope)
+{
+	token->scope = scope;
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+	} while (c != EOF && c != '\n');
+	return c;
+}
+
+static int skipMultiComment (void)
+{
+	int c = getcFromInputFile ();
+	int next = getcFromInputFile ();
+
+	while (c != EOF && !(c == '*' && next == '/'))
+	{
+		c = next;
+		next = getcFromInputFile ();
+	}
+
+	return getcFromInputFile ();
+}
+
+static void parseString (vString *const string, const int delimiter)
+{
+	while (true)
+	{
+		int c = getcFromInputFile ();
+
+		if (c == '\\' && (c = getcFromInputFile ()) != EOF)
+			vStringPut (string, c);
+		else if (c == EOF || c == delimiter)
+			break;
+		else
+			vStringPut (string, c);
+	}
+}
+
+static void parseIdentifier (vString *const string, const int firstChar)
+{
+	int c = firstChar;
+	do
+	{
+		vStringPut (string, c);
+		c = getcFromInputFile ();
+	} while (isIdentChar (c));
+	ungetcToInputFile (c);
+}
+
+static void readToken (tokenInfo *const token)
+{
+	int c;
+
+	token->type		= TOKEN_UNDEFINED;
+	vStringClear (token->string);
+
+getNextChar:
+
+	c = getcFromInputFile ();
+	c = skipWhitespace (c);
+
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	switch (c)
+	{
+		case EOF: token->type = TOKEN_EOF;					break;
+		case '(': token->type = TOKEN_OPEN_PAREN;			break;
+		case ')': token->type = TOKEN_CLOSE_PAREN;			break;
+		case ';': token->type = TOKEN_SEMICOLON;			break;
+		case ',': token->type = TOKEN_COMMA;				break;
+		case '.':
+		{
+			/* Handle ... (spread) operator */
+			int c1 = getcFromInputFile ();
+			int c2 = getcFromInputFile ();
+			if (c1 == '.' && c2 == '.')
+			{
+				token->type = TOKEN_SPREAD;
+			}
+			else
+			{
+				ungetcToInputFile (c2);
+				ungetcToInputFile (c1);
+				token->type = TOKEN_PERIOD;
+			}
+			break;
+		}
+		case ':': token->type = TOKEN_COLON;				break;
+		case '{': token->type = TOKEN_OPEN_CURLY;			break;
+		case '}': token->type = TOKEN_CLOSE_CURLY;			break;
+		case '<': token->type = TOKEN_OPEN_ANGLE;			break;
+		case '>': token->type = TOKEN_CLOSE_ANGLE;			break;
+		case '=': token->type = TOKEN_EQUAL_SIGN;			break;
+		case '[': token->type = TOKEN_OPEN_SQUARE;			break;
+		case ']': token->type = TOKEN_CLOSE_SQUARE;			break;
+
+		case '\'':
+		case '"':
+			token->type = TOKEN_STRING;
+			parseString (token->string, c);
+			break;
+
+		case '/':
+		{
+			int d = getcFromInputFile ();
+			if (d == '/')  /* single-line comment */
+			{
+				skipSingleComment ();
+				goto getNextChar;
+			}
+			else if (d == '*')  /* multi-line comment */
+			{
+				skipMultiComment ();
+				goto getNextChar;
+			}
+			else
+			{
+				ungetcToInputFile (d);
+				token->type = TOKEN_UNDEFINED;
+			}
+			break;
+		}
+
+		case '@':  /* decorator */
+			token->type = TOKEN_DECORATOR;
+			c = getcFromInputFile ();
+			parseIdentifier (token->string, c);
+			break;
+
+		default:
+			if (! isIdentChar (c))
+				token->type = TOKEN_UNDEFINED;
+			else
+			{
+				parseIdentifier (token->string, c);
+                token->keyword = lookupCaseKeyword (
+                    vStringValue (token->string), getInputLanguage ());
+                if (token->keyword == KEYWORD_NONE)
+                    token->type = TOKEN_IDENTIFIER;
+                else
+                    token->type = TOKEN_KEYWORD;
+			}
+			break;
+	}
+}
+
+static void initTypeSpecEntry (tagEntryInfo *const e, const tokenInfo *const token,
+							  const typeSpecKind kind)
+{
+	initTagEntry (e, vStringValue (token->string), kind);
+
+	e->lineNumber	= token->lineNumber;
+	e->filePosition	= token->filePosition;
+
+	e->extensionFields.scopeIndex = token->scope;
+}
+
+static int makeTypeSpecTag (const tokenInfo *const token, const typeSpecKind kind)
+{
+	tagEntryInfo e;
+	initTypeSpecEntry (&e, token, kind);
+	return makeTagEntry (&e);
+}
+
+static void enterScope (tokenInfo *const parentToken, int scope);
+
+static void skipTypeParameters(tokenInfo *const token)
+{
+	int depth = 1;
+
+	while (depth > 0 && token->type != TOKEN_EOF)
+	{
+		readToken(token);
+
+		if (token->type == TOKEN_OPEN_ANGLE)
+			depth++;
+		else if (token->type == TOKEN_CLOSE_ANGLE)
+			depth--;
+	}
+}
+
+static void skipEntityBody(tokenInfo *const token)
+{
+	int depth = 1;
+
+	while (depth > 0 && token->type != TOKEN_EOF)
+	{
+		readToken(token);
+
+		if (token->type == TOKEN_OPEN_CURLY)
+			depth++;
+		else if (token->type == TOKEN_CLOSE_CURLY)
+			depth--;
+	}
+}
+
+static void skipToSemicolon(tokenInfo *const token)
+{
+	while (token->type != TOKEN_SEMICOLON && token->type != TOKEN_EOF)
+	{
+		readToken(token);
+	}
+}
+
+static void parseNamespace(tokenInfo *const token)
+{
+	vString *name = vStringNew();
+
+	/* Read namespace components */
+	do
+	{
+		readToken(token);
+
+		if (token->type == TOKEN_IDENTIFIER)
+		{
+			if (!vStringIsEmpty(name))
+				vStringCatS(name, SCOPE_SEPARATOR);
+			vStringCat(name, token->string);
+
+			readToken(token);
+		}
+		else
+		{
+			break;
+		}
+	} while (token->type == TOKEN_PERIOD);
+
+	/* Create namespace tag */
+	vStringCopy(token->string, name);
+	int namespaceIndex = makeTypeSpecTag(token, K_NAMESPACE);
+
+	/* Parse namespace body */
+	if (token->type == TOKEN_SEMICOLON)
+	{
+		/* Empty namespace, just a declaration */
+	}
+	else if (token->type == TOKEN_OPEN_CURLY)
+	{
+		enterScope(token, namespaceIndex);
+	}
+
+	vStringDelete(name);
+}
+
+static void parseEnum(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		int enumIndex = makeTypeSpecTag(token, K_ENUM);
+		readToken(token);
+
+		if (token->type == TOKEN_OPEN_CURLY)
+		{
+			enterScope(token, enumIndex);
+		}
+	}
+}
+
+static void parseProperty(tokenInfo *const token)
+{
+	if (token->type == TOKEN_IDENTIFIER || token->type == TOKEN_STRING)
+	{
+		/* This is a property */
+		makeTypeSpecTag(token, K_PROPERTY);
+
+		/* Skip type declaration, modifiers, etc. until ; or , */
+		while (token->type != TOKEN_SEMICOLON &&
+			   token->type != TOKEN_COMMA &&
+			   token->type != TOKEN_CLOSE_CURLY &&
+			   token->type != TOKEN_EOF)
+		{
+			readToken(token);
+		}
+	}
+}
+
+static void parseOperation(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		makeTypeSpecTag(token, K_OPERATION);
+		readToken(token);
+
+		/* Skip generic parameters if any */
+		if (token->type == TOKEN_OPEN_ANGLE)
+		{
+			skipTypeParameters(token);
+		}
+
+		/* Handle "is" relationship */
+		if (token->type == TOKEN_KEYWORD && token->keyword == KEYWORD_is)
+		{
+			/* Skip expression after "is" */
+			do {
+				readToken(token);
path: parsers/typespec.c
user: login: masatake
body: This branch is not tested.
created_at: 2025-05-15T19:51:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,708 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enumeration names" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import,
+	KEYWORD_is,
+	KEYWORD_extends
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import },
+	{ "is",         KEYWORD_is },
+	{ "extends",    KEYWORD_extends }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	int				scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= CORK_NIL;
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src,
+					   bool copyScope)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+	if (copyScope)
+		dest->scope = src->scope;
+}
+
+static void setScope (tokenInfo *const token, int scope)
+{
+	token->scope = scope;
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+	} while (c != EOF && c != '\n');
+	return c;
+}
+
+static int skipMultiComment (void)
+{
+	int c = getcFromInputFile ();
+	int next = getcFromInputFile ();
+
+	while (c != EOF && !(c == '*' && next == '/'))
+	{
+		c = next;
+		next = getcFromInputFile ();
+	}
+
+	return getcFromInputFile ();
+}
+
+static void parseString (vString *const string, const int delimiter)
+{
+	while (true)
+	{
+		int c = getcFromInputFile ();
+
+		if (c == '\\' && (c = getcFromInputFile ()) != EOF)
+			vStringPut (string, c);
+		else if (c == EOF || c == delimiter)
+			break;
+		else
+			vStringPut (string, c);
+	}
+}
+
+static void parseIdentifier (vString *const string, const int firstChar)
+{
+	int c = firstChar;
+	do
+	{
+		vStringPut (string, c);
+		c = getcFromInputFile ();
+	} while (isIdentChar (c));
+	ungetcToInputFile (c);
+}
+
+static void readToken (tokenInfo *const token)
+{
+	int c;
+
+	token->type		= TOKEN_UNDEFINED;
+	vStringClear (token->string);
+
+getNextChar:
+
+	c = getcFromInputFile ();
+	c = skipWhitespace (c);
+
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	switch (c)
+	{
+		case EOF: token->type = TOKEN_EOF;					break;
+		case '(': token->type = TOKEN_OPEN_PAREN;			break;
+		case ')': token->type = TOKEN_CLOSE_PAREN;			break;
+		case ';': token->type = TOKEN_SEMICOLON;			break;
+		case ',': token->type = TOKEN_COMMA;				break;
+		case '.':
+		{
+			/* Handle ... (spread) operator */
+			int c1 = getcFromInputFile ();
+			int c2 = getcFromInputFile ();
+			if (c1 == '.' && c2 == '.')
+			{
+				token->type = TOKEN_SPREAD;
+			}
+			else
+			{
+				ungetcToInputFile (c2);
+				ungetcToInputFile (c1);
+				token->type = TOKEN_PERIOD;
+			}
+			break;
+		}
+		case ':': token->type = TOKEN_COLON;				break;
+		case '{': token->type = TOKEN_OPEN_CURLY;			break;
+		case '}': token->type = TOKEN_CLOSE_CURLY;			break;
+		case '<': token->type = TOKEN_OPEN_ANGLE;			break;
+		case '>': token->type = TOKEN_CLOSE_ANGLE;			break;
+		case '=': token->type = TOKEN_EQUAL_SIGN;			break;
+		case '[': token->type = TOKEN_OPEN_SQUARE;			break;
+		case ']': token->type = TOKEN_CLOSE_SQUARE;			break;
+
+		case '\'':
+		case '"':
+			token->type = TOKEN_STRING;
+			parseString (token->string, c);
+			break;
+
+		case '/':
+		{
+			int d = getcFromInputFile ();
+			if (d == '/')  /* single-line comment */
+			{
+				skipSingleComment ();
+				goto getNextChar;
+			}
+			else if (d == '*')  /* multi-line comment */
+			{
+				skipMultiComment ();
+				goto getNextChar;
+			}
+			else
+			{
+				ungetcToInputFile (d);
+				token->type = TOKEN_UNDEFINED;
+			}
+			break;
+		}
+
+		case '@':  /* decorator */
+			token->type = TOKEN_DECORATOR;
+			c = getcFromInputFile ();
+			parseIdentifier (token->string, c);
+			break;
+
+		default:
+			if (! isIdentChar (c))
+				token->type = TOKEN_UNDEFINED;
+			else
+			{
+				parseIdentifier (token->string, c);
+                token->keyword = lookupCaseKeyword (
+                    vStringValue (token->string), getInputLanguage ());
+                if (token->keyword == KEYWORD_NONE)
+                    token->type = TOKEN_IDENTIFIER;
+                else
+                    token->type = TOKEN_KEYWORD;
path: parsers/typespec.c
user: login: masatake
body: The indentation looks strange.
created_at: 2025-05-15T19:55:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,83 @@
+import "@typespec/http";
+import "./common.tsp";
+
+using TypeSpec;
+using TypeSpec.Rest;
+
+@armProviderNamespace
+@service(#{ title: "Name.Space" })
+@versioned(Versions)
+@armCommonTypesVersion(CommonTypes.Versions.v3)
+namespace Name.Space;
+
+@doc("API Versions")
+enum Versions {
path: Units/parser-typespec.r/simple-typespec.d/input.tsp
user: login: iaalm
body: Yes,updating
created_at: 2025-05-25T05:35:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+import "@typespec/http";
+import "./common.tsp";
+
+using TypeSpec;
+using TypeSpec.Rest;
+
+@armProviderNamespace
+@service(#{ title: "Name.Space" })
+@versioned(Versions)
+@armCommonTypesVersion(CommonTypes.Versions.v3)
+namespace Name.Space;
+
+@doc("API Versions")
+enum Versions {
+  @doc("May 01, 2024 Preview API Version")
+  v2024_05_01_preview: "2024-05-01-preview",
path: Units/parser-typespec.r/simple-typespec.d/input.tsp
user: login: iaalm
body: Yes, I have thinked about this. I can do in another PR. BTW, @masatake , could you suggest TypeSpecKinds for this type? I don't find much examples of such kind.
created_at: 2025-05-25T05:39:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,682 @@
+/*
+*   Copyright (c) 2025, Kaisheng Xu
+*
+*   This source code is released for free distribution under the terms of the
+*   GNU General Public License version 2 or (at your option) any later version.
+*
+*   This module contains code for generating tags for TypeSpec language files
+*   (https://microsoft.github.io/typespec/)
+*/
+
+/*
+*   INCLUDE FILES
+*/
+#include "general.h"  /* must always come first */
+#include "debug.h"
+#include "parse.h"
+#include "read.h"
+#include "vstring.h"
+#include "keyword.h"
+#include "entry.h"
+#include "routines.h"
+#include <string.h>
+
+#define SCOPE_SEPARATOR "."
+
+typedef enum {
+	K_NAMESPACE,
+	K_ENUM,
+	K_OPERATION,
+	K_INTERFACE,
+	K_MODEL,
+	K_UNION,
+	K_ALIAS,
+	K_PROPERTY,
+	COUNT_KIND
+} typeSpecKind;
+
+static kindDefinition TypeSpecKinds[COUNT_KIND] = {
+	{ true, 'n', "namespace",	"namespaces" },
+	{ true, 'g', "enum",		"enumeration names" },
+	{ true, 'o', "operation",	"operations" },
+	{ true, 'i', "interface",	"interfaces" },
+	{ true, 'm', "model",		"models" },
+	{ true, 'u', "union",		"unions" },
+	{ true, 'a', "alias",		"aliases" },
+	{ true, 'p', "property",	"properties" }
+};
+
+typedef enum eTokenType {
+	TOKEN_UNDEFINED,
+	TOKEN_EOF,
+	TOKEN_OPEN_PAREN,
+	TOKEN_CLOSE_PAREN,
+	TOKEN_SEMICOLON,
+	TOKEN_COLON,
+	TOKEN_COMMA,
+	TOKEN_KEYWORD,
+	TOKEN_IDENTIFIER,
+	TOKEN_STRING,
+	TOKEN_PERIOD,
+	TOKEN_OPEN_CURLY,
+	TOKEN_CLOSE_CURLY,
+	TOKEN_OPEN_ANGLE,
+	TOKEN_CLOSE_ANGLE,
+	TOKEN_EQUAL_SIGN,
+	TOKEN_OPEN_SQUARE,
+	TOKEN_CLOSE_SQUARE,
+	TOKEN_DECORATOR,
+	TOKEN_SPREAD
+} tokenType;
+
+enum {
+	KEYWORD_namespace,
+	KEYWORD_enum,
+	KEYWORD_op,
+	KEYWORD_interface,
+	KEYWORD_model,
+	KEYWORD_union,
+	KEYWORD_alias,
+	KEYWORD_using,
+	KEYWORD_import,
+	KEYWORD_is,
+	KEYWORD_extends
+};
+
+/* We need an integer that is not an unsigned to allow KEYWORD_NONE. */
+typedef int keywordId;
+
+static const keywordTable TypeSpecKeywordTable[] = {
+	{ "namespace",	KEYWORD_namespace },
+	{ "enum",		KEYWORD_enum },
+	{ "op",			KEYWORD_op },
+	{ "interface",	KEYWORD_interface },
+	{ "model",		KEYWORD_model },
+	{ "union",		KEYWORD_union },
+	{ "alias",		KEYWORD_alias },
+	{ "using",		KEYWORD_using },
+	{ "import",		KEYWORD_import },
+	{ "is",         KEYWORD_is },
+	{ "extends",    KEYWORD_extends }
+};
+
+typedef struct {
+	tokenType		type;
+	keywordId		keyword;
+	vString *		string;
+	int				scope;
+	unsigned long	lineNumber;
+	MIOPos			filePosition;
+} tokenInfo;
+
+static tokenInfo *newToken (void)
+{
+	tokenInfo *const token = xMalloc (1, tokenInfo);
+
+	token->type			= TOKEN_UNDEFINED;
+	token->keyword		= KEYWORD_NONE;
+	token->string		= vStringNew ();
+	token->scope		= CORK_NIL;
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	return token;
+}
+
+static void deleteToken (tokenInfo *const token)
+{
+	vStringDelete (token->string);
+	eFree (token);
+}
+
+static void copyToken (tokenInfo *const dest, const tokenInfo *const src)
+{
+	dest->lineNumber = src->lineNumber;
+	dest->filePosition = src->filePosition;
+	dest->type = src->type;
+	vStringCopy (dest->string, src->string);
+}
+
+static void setScope (tokenInfo *const token, int scope)
+{
+	token->scope = scope;
+}
+
+static bool isIdentChar (const int c)
+{
+	return (isalnum (c) || c == '_' || c == '$' || c >= 0x80);
+}
+
+static bool isSpace (int c)
+{
+	return (c == '\t' || c == ' ' || c == '\v' ||
+			c == '\n' || c == '\r' || c == '\f');
+}
+
+static int skipWhitespace (int c)
+{
+	while (isSpace (c))
+		c = getcFromInputFile ();
+	return c;
+}
+
+static int skipSingleComment (void)
+{
+	int c;
+	do
+	{
+		c = getcFromInputFile ();
+	} while (c != EOF && c != '\n');
+	return c;
+}
+
+static int skipMultiComment (void)
+{
+	int c = getcFromInputFile ();
+	int next = getcFromInputFile ();
+
+	while (c != EOF && !(c == '*' && next == '/'))
+	{
+		c = next;
+		next = getcFromInputFile ();
+	}
+
+	return getcFromInputFile ();
+}
+
+static void parseString (vString *const string, const int delimiter)
+{
+	while (true)
+	{
+		int c = getcFromInputFile ();
+
+		if (c == '\\' && (c = getcFromInputFile ()) != EOF)
+			vStringPut (string, c);
+		else if (c == EOF || c == delimiter)
+			break;
+		else
+			vStringPut (string, c);
+	}
+}
+
+static void parseIdentifier (vString *const string, const int firstChar)
+{
+	int c = firstChar;
+	do
+	{
+		vStringPut (string, c);
+		c = getcFromInputFile ();
+	} while (isIdentChar (c));
+	ungetcToInputFile (c);
+}
+
+static void readToken (tokenInfo *const token)
+{
+	int c;
+
+	token->type		= TOKEN_UNDEFINED;
+	vStringClear (token->string);
+
+getNextChar:
+
+	c = getcFromInputFile ();
+	c = skipWhitespace (c);
+
+	token->lineNumber   = getInputLineNumber ();
+	token->filePosition = getInputFilePosition ();
+
+	switch (c)
+	{
+		case EOF: token->type = TOKEN_EOF;					break;
+		case '(': token->type = TOKEN_OPEN_PAREN;			break;
+		case ')': token->type = TOKEN_CLOSE_PAREN;			break;
+		case ';': token->type = TOKEN_SEMICOLON;			break;
+		case ',': token->type = TOKEN_COMMA;				break;
+		case '.':
+		{
+			/* Handle ... (spread) operator */
+			int c1 = getcFromInputFile ();
+			int c2 = getcFromInputFile ();
+			if (c1 == '.' && c2 == '.')
+			{
+				token->type = TOKEN_SPREAD;
+			}
+			else
+			{
+				ungetcToInputFile (c2);
+				ungetcToInputFile (c1);
+				token->type = TOKEN_PERIOD;
+			}
+			break;
+		}
+		case ':': token->type = TOKEN_COLON;				break;
+		case '{': token->type = TOKEN_OPEN_CURLY;			break;
+		case '}': token->type = TOKEN_CLOSE_CURLY;			break;
+		case '<': token->type = TOKEN_OPEN_ANGLE;			break;
+		case '>': token->type = TOKEN_CLOSE_ANGLE;			break;
+		case '=': token->type = TOKEN_EQUAL_SIGN;			break;
+		case '[': token->type = TOKEN_OPEN_SQUARE;			break;
+		case ']': token->type = TOKEN_CLOSE_SQUARE;			break;
+
+		case '\'':
+		case '"':
+			token->type = TOKEN_STRING;
+			parseString (token->string, c);
+			break;
+
+		case '/':
+		{
+			int d = getcFromInputFile ();
+			if (d == '/')  /* single-line comment */
+			{
+				skipSingleComment ();
+				goto getNextChar;
+			}
+			else if (d == '*')  /* multi-line comment */
+			{
+				skipMultiComment ();
+				goto getNextChar;
+			}
+			else
+			{
+				ungetcToInputFile (d);
+				token->type = TOKEN_UNDEFINED;
+			}
+			break;
+		}
+
+		case '@':  /* decorator */
+			token->type = TOKEN_DECORATOR;
+			c = getcFromInputFile ();
+			parseIdentifier (token->string, c);
+			break;
+
+		default:
+			if (! isIdentChar (c))
+				token->type = TOKEN_UNDEFINED;
+			else
+			{
+				parseIdentifier (token->string, c);
+                token->keyword = lookupCaseKeyword (
+                    vStringValue (token->string), getInputLanguage ());
+                if (token->keyword == KEYWORD_NONE)
+                    token->type = TOKEN_IDENTIFIER;
+                else
+                    token->type = TOKEN_KEYWORD;
+			}
+			break;
+	}
+}
+
+static void initTypeSpecEntry (tagEntryInfo *const e, const tokenInfo *const token,
+							  const typeSpecKind kind)
+{
+	initTagEntry (e, vStringValue (token->string), kind);
+
+	e->lineNumber	= token->lineNumber;
+	e->filePosition	= token->filePosition;
+
+	e->extensionFields.scopeIndex = token->scope;
+}
+
+static int makeTypeSpecTag (const tokenInfo *const token, const typeSpecKind kind)
+{
+	tagEntryInfo e;
+	initTypeSpecEntry (&e, token, kind);
+	return makeTagEntry (&e);
+}
+
+static void enterScope (tokenInfo *const parentToken, int scope);
+
+static void skipTypeParameters(tokenInfo *const token)
+{
+	int depth = 1;
+
+	while (depth > 0 && token->type != TOKEN_EOF)
+	{
+		readToken(token);
+
+		if (token->type == TOKEN_OPEN_ANGLE)
+			depth++;
+		else if (token->type == TOKEN_CLOSE_ANGLE)
+			depth--;
+	}
+	readToken(token);
+}
+
+static void skipToSemicolon(tokenInfo *const token)
+{
+	while (token->type != TOKEN_SEMICOLON && token->type != TOKEN_EOF)
+	{
+		readToken(token);
+	}
+}
+
+static void parseNamespace(tokenInfo *const token)
+{
+	vString *name = vStringNew();
+
+	/* Read namespace components */
+	do
+	{
+		readToken(token);
+
+		if (token->type == TOKEN_IDENTIFIER)
+		{
+			if (!vStringIsEmpty(name))
+				vStringCatS(name, SCOPE_SEPARATOR);
+			vStringCat(name, token->string);
+
+			readToken(token);
+		}
+		else
+		{
+			break;
+		}
+	} while (token->type == TOKEN_PERIOD);
+
+	/* Create namespace tag */
+	vStringCopy(token->string, name);
+	int namespaceIndex = makeTypeSpecTag(token, K_NAMESPACE);
+
+	/* Parse namespace body */
+	if (token->type == TOKEN_SEMICOLON || token->type == TOKEN_OPEN_CURLY)
+	{
+		enterScope(token, namespaceIndex);
+	}
+
+	vStringDelete(name);
+}
+
+static void parseEnum(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		int enumIndex = makeTypeSpecTag(token, K_ENUM);
+		readToken(token);
+
+		if (token->type == TOKEN_OPEN_CURLY)
+		{
+			enterScope(token, enumIndex);
+		}
+	}
+}
+
+static void parseProperty(tokenInfo *const token)
+{
+	if (token->type == TOKEN_IDENTIFIER || token->type == TOKEN_STRING)
+	{
+		/* This is a property */
+		makeTypeSpecTag(token, K_PROPERTY);
+
+		/* Skip type declaration, modifiers, etc. until ; or , */
+		while (token->type != TOKEN_SEMICOLON &&
+			   token->type != TOKEN_COMMA &&
+			   token->type != TOKEN_CLOSE_CURLY &&
+			   token->type != TOKEN_EOF)
+		{
+		/* Skip generic parameters if any */
+			readToken(token);
+			if (token->type == TOKEN_OPEN_ANGLE)
+			{
+				skipTypeParameters(token);
+			}
+		}
+	}
+}
+
+static void parseOperation(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		makeTypeSpecTag(token, K_OPERATION);
+		readToken(token);
+
+		/* Skip generic parameters if any */
+		if (token->type == TOKEN_OPEN_ANGLE)
+		{
+			skipTypeParameters(token);
+		}
+
+		/* Handle "is" relationship */
+		if (token->type == TOKEN_KEYWORD && token->keyword == KEYWORD_is)
+		{
+			/* Skip expression after "is" */
+			do {
+				readToken(token);
+
+				if (token->type == TOKEN_OPEN_ANGLE)
+				{
+					skipTypeParameters(token);
+				}
+
+				if (token->type == TOKEN_SEMICOLON)
+					break;
+
+			} while (token->type != TOKEN_SEMICOLON && token->type != TOKEN_EOF);
+		}
+	}
+}
+
+static void parseInterface(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		int ifaceIndex = makeTypeSpecTag(token, K_INTERFACE);
+		readToken(token);
+
+		/* Skip extends clause */
+		if (token->type == TOKEN_KEYWORD && token->keyword == KEYWORD_extends)
+		{
+			while (token->type != TOKEN_OPEN_CURLY && token->type != TOKEN_EOF)
+			{
+				readToken(token);
+			}
+		}
+
+		if (token->type == TOKEN_OPEN_CURLY)
+		{
+			enterScope(token, ifaceIndex);
+		}
+	}
+}
+
+static void parseModel(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		int modelIndex = makeTypeSpecTag(token, K_MODEL);
+		readToken(token);
+
+		/* Skip extends clause if any */
+		if (token->type == TOKEN_KEYWORD && token->keyword == KEYWORD_extends)
+		{
+			while (token->type != TOKEN_OPEN_CURLY && token->type != TOKEN_EOF)
+			{
+				readToken(token);
+			}
+		}
+
+		if (token->type == TOKEN_OPEN_CURLY)
+		{
+			enterScope(token, modelIndex);
+		}
+	}
+}
+
+static void parseUnion(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		int unionIndex = makeTypeSpecTag(token, K_UNION);
+		readToken(token);
+
+		if (token->type == TOKEN_OPEN_CURLY)
+		{
+			enterScope(token, unionIndex);
+		}
+	}
+}
+
+static void parseAlias(tokenInfo *const token)
+{
+	readToken(token);
+
+	if (token->type == TOKEN_IDENTIFIER)
+	{
+		makeTypeSpecTag(token, K_ALIAS);
+		readToken(token);
+
+		/* Skip generic parameters if any */
+		if (token->type == TOKEN_OPEN_ANGLE)
+		{
+			skipTypeParameters(token);
+		}
+
+		/* Skip the rest until semicolon */
+		skipToSemicolon(token);
+	}
+}
+
+static void enterScope (tokenInfo *const parentToken, int scope)
+{
+	tokenInfo *token = newToken ();
+    tagEntryInfo *e_parent;
path: parsers/typespec.c
user: login: masatake
body: The indentation offset looks wrong. You may use both whitespaces and tabs mixly.
created_at: 2025-05-25T23:28:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,83 @@
+import "@typespec/http";
+import "./common.tsp";
+
+using TypeSpec;
+using TypeSpec.Rest;
+
+@armProviderNamespace
+@service(#{ title: "Name.Space" })
+@versioned(Versions)
+@armCommonTypesVersion(CommonTypes.Versions.v3)
+namespace Name.Space;
+
+@doc("API Versions")
+enum Versions {
+  @doc("May 01, 2024 Preview API Version")
+  v2024_05_01_preview: "2024-05-01-preview",
path: Units/parser-typespec.r/simple-typespec.d/input.tsp
user: login: masatake
body: As far as reading https://typespec.io/docs/language-basics/enums/, the enum in TypeSpec is not so different from that in C. So we can reuse what the C parser defines.
```
$ ./ctags --list-kinds-full=C | grep enum
e       enumerator yes     no      0      C      enumerators (values inside an enumeration)
g       enum       yes     no      0      C      enumeration names
```

`e,enumerator,enumerators (values inside an enumeration)` looks suitable here.
created_at: 2025-05-25T23:37:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,83 @@
+import "@typespec/http";
+import "./common.tsp";
+
+using TypeSpec;
+using TypeSpec.Rest;
+
+@armProviderNamespace
+@service(#{ title: "Name.Space" })
+@versioned(Versions)
+@armCommonTypesVersion(CommonTypes.Versions.v3)
+namespace Name.Space;
+
+@doc("API Versions")
+enum Versions {
+  @doc("May 01, 2024 Preview API Version")
+  v2024_05_01_preview: "2024-05-01-preview",
path: Units/parser-typespec.r/simple-typespec.d/input.tsp
user: login: masatake
body: We should refer to TypeScript instead of C.
```
$ ./ctags --list-kinds-full=TypeScript | grep enum
e       enumerator yes     no      0      NONE   enumerators (values inside an enumeration)
g       enum       yes     no      0      NONE   enums
```

TypeScript also uses `g`.
created_at: 2025-05-25T23:43:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,83 @@
+import "@typespec/http";
+import "./common.tsp";
+
+using TypeSpec;
+using TypeSpec.Rest;
+
+@armProviderNamespace
+@service(#{ title: "Name.Space" })
+@versioned(Versions)
+@armCommonTypesVersion(CommonTypes.Versions.v3)
+namespace Name.Space;
+
+@doc("API Versions")
+enum Versions {
path: Units/parser-typespec.r/simple-typespec.d/input.tsp
user: login: masatake
body: I read the test case. This looks solved.
created_at: 2025-05-25T23:47:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,83 @@
+import "@typespec/http";
+import "./common.tsp";
+
+using TypeSpec;
+using TypeSpec.Rest;
+
+@armProviderNamespace
+@service(#{ title: "Name.Space" })
+@versioned(Versions)
+@armCommonTypesVersion(CommonTypes.Versions.v3)
+namespace Name.Space;
+
+@doc("API Versions")
+enum Versions {
+  @doc("May 01, 2024 Preview API Version")
+  v2024_05_01_preview: "2024-05-01-preview",
+}
+
+@summary("Example operation summary for demonstration purposes.")
+@doc("""
+  Example
+  multi-line
+  documentation.
+  """)
+@returnsDoc("Example return documentation")
+@post
+op OpA<T extends TypeSpec.Reflection.Model> is OpB<
+  T & ModelA,
+  ModelB<ModelA[]>
+>;
+
+interface InterfaceA extends InterfaceB {
path: Units/parser-typespec.r/simple-typespec.d/input.tsp
user: login: iaalm
body: Yes, I can do it in another PR
created_at: 2025-05-27T03:37:08Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-05-08T11:03:25Z
body: ## [Codecov](https://app.codecov.io/gh/universal-ctags/ctags/pull/4243?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=universal-ctags) Report
Attention: Patch coverage is `98.06452%` with `6 lines` in your changes missing coverage. Please review.
> Project coverage is 85.95%. Comparing base [(`7dbc4db`)](https://app.codecov.io/gh/universal-ctags/ctags/commit/7dbc4db5710f3a0be5eb43a6b075499ee2433074?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=universal-ctags) to head [(`846da8d`)](https://app.codecov.io/gh/universal-ctags/ctags/commit/846da8d9e3eae54b5c0ebc1afed5bdc9c81e3a12?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=universal-ctags).
> Report is 2 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/universal-ctags/ctags/pull/4243?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=universal-ctags) | Patch % | Lines |
|---|---|---|
| [parsers/typespec.c](https://app.codecov.io/gh/universal-ctags/ctags/pull/4243?src=pr&el=tree&filepath=parsers%2Ftypespec.c&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=universal-ctags#diff-cGFyc2Vycy90eXBlc3BlYy5j) | 98.06% | [6 Missing :warning: ](https://app.codecov.io/gh/universal-ctags/ctags/pull/4243?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=universal-ctags) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4243      +/-   ##
==========================================
+ Coverage   85.89%   85.95%   +0.05%     
==========================================
  Files         245      246       +1     
  Lines       63149    63459     +310     
==========================================
+ Hits        54240    54544     +304     
- Misses       8909     8915       +6     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/universal-ctags/ctags/pull/4243?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=universal-ctags).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=universal-ctags).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: masatake
created_at: 2025-05-08T16:58:04Z
body: Let's use CORK API.
See the latest two commits at https://github.com/masatake/ctags/commits/tsp-20250509/ .


author_association: MEMBER
user: login: masatake
created_at: 2025-05-15T19:58:44Z
body: Thank you for updating.

Could you add the name of the new parser to docs/news/HEAD.rst?
author_association: MEMBER
user: login: masatake
created_at: 2025-05-27T04:45:25Z
body: Thank you for updating.

Could you add the name of the new parser to docs/news/HEAD.rst?
Please remove whitespace in input.tsp

![image](https://github.com/user-attachments/assets/3d891091-5097-4637-896a-6131a483f253)

author_association: MEMBER
user: login: masatake
created_at: 2025-05-27T09:43:34Z
body: Thank you very much.
author_association: MEMBER