PPiotr Fusik[fut] Fix C# fut.
f0d33ffb创建于 27 天前历史提交
// GenJs.fu - JavaScript code generator
//
// Copyright (C) 2011-2026  Piotr Fusik
//
// This file is part of Fusion Transpiler,
// see https://github.com/fusionlanguage/fut
//
// Fusion Transpiler is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Fusion Transpiler is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Fusion Transpiler.  If not, see http://www.gnu.org/licenses/

public class GenJsNoModule : GenBase
{
	bool StringWriter = false;
	bool ConsoleReadLine = false;

	protected override string GetTargetName() => "JavaScript";

	void WriteCamelCaseNotKeyword!(string name)
	{
		WriteCamelCase(name);
		switch (name) {
		case "Arguments":
		case "Constructor":
		case "arguments":
		case "await":
		case "catch":
		case "debugger":
		case "delete":
		case "export":
		case "extends":
		case "finally":
		case "function":
		case "implements":
		case "import":
		case "instanceof":
		case "interface":
		case "let":
		case "package":
		case "private":
		case "super":
		case "try":
		case "typeof":
		case "var":
		case "with":
		case "yield":
			WriteChar('_');
			break;
		default:
			break;
		}
	}

	protected override void WriteName!(FuSymbol symbol)
	{
		switch (symbol) {
		case FuContainerType:
			Write(symbol.Name);
			break;
		case FuConst konst:
			if (konst.Visibility == FuVisibility.Private)
				WriteChar('#');
			WriteUppercaseConstName(konst);
			break;
		case FuVar:
			WriteCamelCaseNotKeyword(symbol.Name);
			break;
		case FuMember member:
			if (member.Visibility == FuVisibility.Private) {
				WriteChar('#');
				WriteCamelCase(symbol.Name);
				if (symbol.Name == "Constructor")
					WriteChar('_');
			}
			else
				WriteCamelCaseNotKeyword(symbol.Name);
			break;
		default:
			assert false;
		}
	}

	protected override void WriteTypeAndName!(FuNamedValue value)
	{
		WriteName(value);
	}

	protected void WriteArrayElementType!(FuType type)
	{
		switch (type.Id) {
		case FuId.SByteRange:
			Write("Int8");
			break;
		case FuId.ByteRange:
			Write("Uint8");
			break;
		case FuId.ShortRange:
			Write("Int16");
			break;
		case FuId.UShortRange:
			Write("Uint16");
			break;
		case FuId.IntType:
		case FuId.NIntType:
			Write("Int32");
			break;
		case FuId.LongType:
			Write("BigInt64");
			break;
		case FuId.FloatType:
			Write("Float32");
			break;
		case FuId.DoubleType:
			Write("Float64");
			break;
		default:
			assert false;
		}
	}

	internal override void VisitAggregateInitializer!(FuAggregateInitializer expr)
	{
		assert expr.Type is FuArrayStorageType array;
		bool numeric = false;
		if (array.GetElementType() is FuNumericType number) {
			Write("new ");
			WriteArrayElementType(number);
			Write("Array(");
			numeric = true;
		}
		Write("[ ");
		WriteCoercedLiterals(null, expr.Items);
		Write(" ]");
		if (numeric)
			WriteChar(')');
	}

	protected override void WriteNew!(FuReadWriteClassType klass, FuPriority parent)
	{
		switch (klass.Class.Id) {
		case FuId.ListClass:
		case FuId.QueueClass:
		case FuId.StackClass:
			Write("[]");
			break;
		case FuId.HashSetClass:
		case FuId.SortedSetClass:
			Write("new Set()");
			break;
		case FuId.DictionaryClass:
		case FuId.SortedDictionaryClass:
			Write("{}");
			break;
		case FuId.OrderedDictionaryClass:
			Write("new Map()");
			break;
		case FuId.LockClass:
			NotSupported(klass, "Lock");
			break;
		default:
			Write("new ");
			if (klass.Class.Id == FuId.StringWriterClass)
				this.StringWriter = true;
			Write(klass.Class.Name);
			Write("()");
			break;
		}
	}

	protected override void WriteNewWithFields!(FuReadWriteClassType type, FuAggregateInitializer init)
	{
		Write("Object.assign(");
		WriteNew(type, FuPriority.Argument);
		WriteChar(',');
		WriteObjectLiteral(init, ": ");
		WriteChar(')');
	}

	protected override void WriteVar!(FuNamedValue def)
	{
		Write(def.Type.IsFinal() && !def.IsAssignableStorage() ? "const " : "let ");
		base.WriteVar(def);
	}

	void WriteInterpolatedLiteral!(string s)
	{
		int i = 0;
		foreach (int c in s) {
			i++;
			if (c == '`'
			 || (c == '$' && i < s.Length && s[i] == '{'))
				WriteChar('\\');
			WriteChar(c);
		}
	}

	internal override void VisitInterpolatedString!(FuInterpolatedString expr, FuPriority parent)
	{
		if (expr.IsToString('u')) {
			WriteCall("String.fromCodePoint", expr.Parts[0].Argument);
			return;
		}
		WriteChar('`');
		foreach (FuInterpolatedPart part in expr.Parts) {
			WriteInterpolatedLiteral(part.Prefix);
			Write("${");
			if (part.Width != 0 || part.Format != ' ') {
				if (part.Format == 'U' || part.Format == 'u')
					WriteCall("String.fromCodePoint", part.Argument);
				else {
					if (part.Argument is FuLiteralLong || part.Argument is FuPrefixExpr) {
						// FIXME: we should rather split FuPriority.Primary into FuPriority.Prefix and FuPriority.Suffix
						WriteChar('(');
						part.Argument.Accept(this, FuPriority.Primary);
						WriteChar(')');
					}
					else
						part.Argument.Accept(this, FuPriority.Primary);
					if (part.Argument.Type is FuNumericType) {
						switch (part.Format) {
						case 'E':
							Write(".toExponential(");
							if (part.Precision >= 0)
								VisitLiteralLong(part.Precision, FuPriority.Argument);
							Write(").toUpperCase()");
							break;
						case 'e':
							Write(".toExponential(");
							if (part.Precision >= 0)
								VisitLiteralLong(part.Precision, FuPriority.Argument);
							WriteChar(')');
							break;
						case 'F':
						case 'f':
							Write(".toFixed(");
							if (part.Precision >= 0)
								VisitLiteralLong(part.Precision, FuPriority.Argument);
							WriteChar(')');
							break;
						case 'X':
							Write(".toString(16).toUpperCase()");
							break;
						case 'x':
							Write(".toString(16)");
							break;
						default:
							Write(".toString()");
							break;
						}
						if (part.Precision >= 0) {
							switch (part.Format) {
							case 'D':
							case 'd':
							case 'X':
							case 'x':
								Write(".padStart(");
								VisitLiteralLong(part.Precision, FuPriority.Argument);
								Write(", \"0\")");
								break;
							default:
								break;
							}
						}
					}
				}
				if (part.Width > 0) {
					Write(".padStart(");
					VisitLiteralLong(part.Width, FuPriority.Argument);
					WriteChar(')');
				}
				else if (part.Width < 0) {
					Write(".padEnd(");
					VisitLiteralLong(-part.Width, FuPriority.Argument);
					WriteChar(')');
				}
			}
			else
				part.Argument.Accept(this, FuPriority.Argument);
			WriteChar('}');
		}
		WriteInterpolatedLiteral(expr.Suffix);
		WriteChar('`');
	}

	protected override void WriteLocalName!(FuSymbol symbol, FuPriority parent)
	{
		if (symbol is FuMember member) {
			if (!member.IsStatic())
				Write("this");
			else if (this.CurrentMethod != null)
				Write(this.CurrentMethod.Parent.Name);
			else if (symbol is FuConst konst) {
				if (konst.Value is FuImplicitEnumValue)
					WriteUppercaseWithUnderscores(konst.Name);
				else
					konst.Value.Accept(this, parent);
				return;
			}
			else
				assert false;
			WriteChar('.');
		}
		WriteName(symbol);
		if (symbol.Parent is FuForeach forEach && forEach.Collection.Type is FuStringType)
			Write(".codePointAt(0)");
	}

	protected override void WriteCoercedInternal!(FuType type, FuExpr expr, FuPriority parent)
	{
		if (type is FuNumericType) { // not FuPrintableType
			if (type.Id == FuId.LongType) {
				if (expr is FuLiteralLong) {
					expr.Accept(this, FuPriority.Primary);
					WriteChar('n');
					return;
				}
				if (expr.Type.Id != FuId.LongType) {
					WriteCall("BigInt", expr);
					return;
				}
			}
			else if (expr.Type.Id == FuId.LongType) {
				WriteCall("Number", expr);
				return;
			}
		}
		expr.Accept(this, parent);
	}

	protected override void WriteNotPromoted!(FuType type, FuExpr expr)
	{
		WriteCoercedInternal(type, expr, FuPriority.Argument);
	}

	protected override void WriteNewArray!(FuType elementType, FuExpr lengthExpr, FuPriority parent)
	{
		if (elementType is FuStorageType) {
			Write("Array.from({ length: ");
			lengthExpr.Accept(this, FuPriority.Argument);
			Write(" }, () => ");
			WriteNewStorage(elementType);
			WriteChar(')');
		}
		else {
			Write("new ");
			if (elementType is FuNumericType)
				WriteArrayElementType(elementType);
			WriteCall("Array", lengthExpr);
		}
	}

	protected override bool HasInitCode(FuNamedValue def) => false;

	protected override void WriteInitCode!(FuNamedValue def)
	{
	}

	protected override void WriteResource!(string name, int length)
	{
		Write("Fu.");
		WriteResourceName(name);
	}

	internal override void VisitSymbolReference!(FuSymbolReference expr, FuPriority parent)
	{
		switch (expr.Symbol.Id) {
		case FuId.ConsoleError:
			Write("process.stderr");
			break;
		case FuId.ConsoleOut:
			Write("process.stdout");
			break;
		case FuId.ListCount:
		case FuId.QueueCount:
		case FuId.StackCount:
			WritePostfix(expr.Left, ".length");
			break;
		case FuId.HashSetCount:
		case FuId.SortedSetCount:
		case FuId.OrderedDictionaryCount:
			WritePostfix(expr.Left, ".size");
			break;
		case FuId.DictionaryCount:
		case FuId.SortedDictionaryCount:
			WriteCall("Object.keys", expr.Left);
			Write(".length");
			break;
		case FuId.MatchStart:
			WritePostfix(expr.Left, ".index");
			break;
		case FuId.MatchEnd:
			if (parent > FuPriority.Add)
				WriteChar('(');
			WritePostfix(expr.Left, ".index + ");
			WritePostfix(expr.Left, "[0].length"); // FIXME: side effect
			if (parent > FuPriority.Add)
				WriteChar(')');
			break;
		case FuId.MatchLength:
			WritePostfix(expr.Left, "[0].length");
			break;
		case FuId.MatchValue:
			WritePostfix(expr.Left, "[0]");
			break;
		case FuId.MathNaN:
			Write("NaN");
			break;
		case FuId.MathNegativeInfinity:
			Write("-Infinity");
			break;
		case FuId.MathPositiveInfinity:
			Write("Infinity");
			break;
		default:
			base.VisitSymbolReference(expr, parent);
			break;
		}
	}

	protected override void WriteStringLength!(FuExpr expr)
	{
		WritePostfix(expr, ".length");
	}

	protected override void WriteCharAt!(FuBinaryExpr expr)
	{
		WriteMethodCall(expr.Left, "charCodeAt", expr.Right);
	}

	protected override void WriteBinaryOperand!(FuExpr expr, FuPriority parent, FuBinaryExpr binary)
	{
		WriteCoerced(binary.IsRel() ? expr.Type : binary.Type, expr, parent);
	}

	void WriteSlice!(FuExpr array, FuExpr offset, FuExpr length, FuPriority parent, string method)
	{
		if (IsWholeArray(array, offset, length))
			array.Accept(this, parent);
		else {
			array.Accept(this, FuPriority.Primary);
			WriteChar('.');
			Write(method);
			WriteChar('(');
			WriteStartEnd(offset, length);
			WriteChar(')');
		}
	}

	protected virtual void WriteDictionaryClearCast!(FuExpr obj)
	{
	}

	void EndWrite!(FuExpr arg)
	{
		if (arg.Type is FuStringType)
			arg.Accept(this, FuPriority.Argument);
		else
			WriteCall("String", arg);
		WriteChar(')');
	}

	void WriteWriteLine!(string method, List<FuExpr#> args)
	{
		Write(method);
		WriteChar('(');
		if (args.Count == 0)
			Write("\"\"");
		else if (args[0].Type.Id == FuId.LongType)
			WriteCall("String", args[0]);
		else
			args[0].Accept(this, FuPriority.Argument);
		WriteChar(')');
	}

	static bool IsIdentifier(string s)
	{
		if (s.Length == 0 || s[0] < 'A')
			return false;
		foreach (int c in s) {
			if (!FuLexer.IsLetterOrDigit(c))
				return false;
		}
		return true;
	}

	void WriteNewRegex!(List<FuExpr#> args, int argIndex)
	{
		FuExpr pattern = args[argIndex];
		if (pattern is FuLiteralString literal) {
			WriteRegexLiteral(literal.Value);
			WriteRegexOptions(args, "", "", "", "i", "m", "s");
		}
		else {
			Write("new RegExp(");
			pattern.Accept(this, FuPriority.Argument);
			WriteRegexOptions(args, ", \"", "", "\"", "i", "m", "s");
			WriteChar(')');
		}
	}

	void WriteTypeofEquals!(FuExpr obj, string name, FuPriority parent)
	{
		if (parent > FuPriority.Equality)
			WriteChar('(');
		Write("typeof(");
		obj.Accept(this, FuPriority.Argument);
		Write(") == \"");
		Write(name);
		WriteChar('"');
		if (parent > FuPriority.Equality)
			WriteChar(')');
	}

	static bool HasLong(List<FuExpr#> args) => args.Any(arg => arg.Type.Id == FuId.LongType);

	void WriteMathMaxMin!(FuMethod method, string name, int op, List<FuExpr#> args)
	{
		if (HasLong(args)) {
			Write("((x, y) => x ");
			WriteChar(op);
			Write(" y ? x : y)");
			WriteInParentheses(args);
		}
		else
			WriteCall(name, args[0], args[1]);
	}

	protected override void WriteCallExpr!(FuType type, FuExpr? obj, FuMethod method, List<FuExpr#> args, FuPriority parent)
	{
		switch (method.Id) {
		case FuId.None:
		case FuId.ClassToString:
		case FuId.StringEndsWith:
		case FuId.StringIndexOf:
		case FuId.StringLastIndexOf:
		case FuId.StringStartsWith:
		case FuId.ArraySortAll:
		case FuId.ListIndexOf:
		case FuId.StackPush:
		case FuId.StackPop:
		case FuId.HashSetAdd:
		case FuId.HashSetClear:
		case FuId.SortedSetAdd:
		case FuId.SortedSetClear:
		case FuId.OrderedDictionaryClear:
		case FuId.StringWriterClear:
		case FuId.StringWriterToString:
		case FuId.MathMethod:
		case FuId.MathLog2:
		case FuId.MathRound:
		case FuId.MathSqrt:
			if (obj == null)
				WriteLocalName(method, FuPriority.Primary);
			else {
				if (IsReferenceTo(obj, FuId.BasePtr))
					Write("super");
				else
					obj.Accept(this, FuPriority.Primary);
				WriteChar('.');
				WriteName(method);
			}
			WriteCoercedArgsInParentheses(method, args);
			break;
		case FuId.EnumFromInt:
			args[0].Accept(this, parent);
			break;
		case FuId.EnumHasFlag:
			WriteEnumHasFlag(obj, args, parent);
			break;
		case FuId.EnumToInt:
			obj.Accept(this, parent);
			break;
		case FuId.IntTryParse:
		case FuId.NIntTryParse:
			Write("(() => { const fuparsed = parseInt(");
			args[0].Accept(this, FuPriority.Argument);
			WriteTryParseRadix(args);
			Write("); ");
			obj.Accept(this, FuPriority.Assign);
			Write(" = isNaN(fuparsed) ? 0 : fuparsed; return !isNaN(fuparsed); })()");
			break;
		case FuId.LongTryParse:
			if (args.Count != 1)
				NotSupported(args[1], "Radix");
			Write("(() => { try { ");
			obj.Accept(this, FuPriority.Assign);
			Write(" = BigInt(");
			args[0].Accept(this, FuPriority.Argument);
			Write("); return true; } catch { ");
			obj.Accept(this, FuPriority.Assign);
			Write(" = 0n; return false; }})()");
			break;
		case FuId.FloatTryParse:
		case FuId.DoubleTryParse:
			Write("(() => { const fuparsed = parseFloat("); // parseFloat ignores trailing invalid characters; Number() does not, but it accepts empty string and bin/oct/hex
			args[0].Accept(this, FuPriority.Argument);
			Write("); ");
			obj.Accept(this, FuPriority.Assign);
			Write(" = isNaN(fuparsed) ? 0 : fuparsed; return !isNaN(fuparsed); })()");
			// FIXME: check float
			break;
		case FuId.StringContains:
		case FuId.ArrayContains:
		case FuId.ListContains:
			WriteMethodCall(obj, "includes", args[0]);
			break;
		case FuId.StringReplace:
			WriteMethodCall(obj, "replaceAll", args[0], args[1]);
			break;
		case FuId.StringSubstring:
			WritePostfix(obj, ".substring(");
			args[0].Accept(this, FuPriority.Argument);
			if (args.Count == 2) {
				Write(", ");
				WriteAdd(args[0], args[1]); // FIXME: side effect
			}
			WriteChar(')');
			break;
		case FuId.StringToLower:
			WritePostfix(obj, ".toLowerCase()");
			break;
		case FuId.StringToUpper:
			WritePostfix(obj, ".toUpperCase()");
			break;
		case FuId.ArrayFillAll:
		case FuId.ArrayFillPart:
			WritePostfix(obj, ".fill(");
			args[0].Accept(this, FuPriority.Argument);
			if (args.Count == 3) {
				Write(", ");
				WriteStartEnd(args[1], args[2]);
			}
			WriteChar(')');
			break;
		case FuId.ArrayCopyTo:
		case FuId.ListCopyTo:
			args[1].Accept(this, FuPriority.Primary);
			if (obj.Type.AsClassType().GetElementType() is FuNumericType) {
				Write(".set(");
				WriteSlice(obj, args[0], args[3], FuPriority.Argument, method.Id == FuId.ArrayCopyTo ? "subarray" : "slice");
				if (!args[2].IsLiteralZero()) {
					Write(", ");
					args[2].Accept(this, FuPriority.Argument);
				}
			}
			else {
				Write(".splice(");
				args[2].Accept(this, FuPriority.Argument);
				Write(", ");
				args[3].Accept(this, FuPriority.Argument);
				Write(", ...");
				WriteSlice(obj, args[0], args[3], FuPriority.Primary, "slice");
			}
			WriteChar(')');
			break;
		case FuId.ArraySortPart:
			WritePostfix(obj, ".subarray(");
			WriteStartEnd(args[0], args[1]);
			Write(").sort()");
			break;
		case FuId.ListAdd:
			WriteListAdd(obj, "push", args);
			break;
		case FuId.ListAddRange:
			WritePostfix(obj, ".push(...");
			args[0].Accept(this, FuPriority.Argument);
			WriteChar(')');
			break;
		case FuId.ListAll:
			WriteMethodCall(obj, "every", args[0]);
			break;
		case FuId.ListAny:
			WriteMethodCall(obj, "some", args[0]);
			break;
		case FuId.ListClear:
		case FuId.QueueClear:
		case FuId.StackClear:
			WritePostfix(obj, ".length = 0");
			break;
		case FuId.ListInsert:
			WriteListInsert(obj, "splice", args, ", 0, ");
			break;
		case FuId.ListLast:
		case FuId.StackPeek:
			WritePostfix(obj, ".at(-1)");
			break;
		case FuId.ListRemoveAt:
			WritePostfix(obj, ".splice(");
			args[0].Accept(this, FuPriority.Argument);
			Write(", 1)");
			break;
		case FuId.ListRemoveRange:
			WriteMethodCall(obj, "splice", args[0], args[1]);
			break;
		case FuId.ListSortAll:
			WritePostfix(obj, ".sort((a, b) => a - b)");
			break;
		case FuId.ListSortPart:
			WritePostfix(obj, ".splice(");
			args[0].Accept(this, FuPriority.Argument);
			Write(", ");
			args[1].Accept(this, FuPriority.Argument);
			Write(", ...");
			WriteSlice(obj, args[0], args[1], FuPriority.Primary, "slice");
			Write(".sort((a, b) => a - b))");
			break;
		case FuId.QueueDequeue:
			WritePostfix(obj, ".shift()");
			break;
		case FuId.QueueEnqueue:
			WriteMethodCall(obj, "push", args[0]);
			break;
		case FuId.QueuePeek:
			WritePostfix(obj, "[0]");
			break;
		case FuId.HashSetContains:
		case FuId.SortedSetContains:
		case FuId.OrderedDictionaryContainsKey:
			WriteMethodCall(obj, "has", args[0]);
			break;
		case FuId.HashSetRemove:
		case FuId.SortedSetRemove:
		case FuId.OrderedDictionaryRemove:
			WriteMethodCall(obj, "delete", args[0]);
			break;
		case FuId.DictionaryAdd:
			WriteDictionaryAdd(obj, args);
			break;
		case FuId.DictionaryClear:
		case FuId.SortedDictionaryClear:
			Write("for (const key in ");
			obj.Accept(this, FuPriority.Argument);
			WriteCharLine(')');
			Write("\tdelete ");
			WritePostfix(obj, "[key"); // FIXME: side effect
			WriteDictionaryClearCast(obj);
			WriteChar(']');
			break;
		case FuId.DictionaryContainsKey:
		case FuId.SortedDictionaryContainsKey:
			WriteMethodCall(obj, "hasOwnProperty", args[0]);
			break;
		case FuId.DictionaryRemove:
		case FuId.SortedDictionaryRemove:
			Write("delete ");
			WriteIndexing(obj, args[0]);
			break;
		case FuId.TextWriterFlush:
			// no operation
			break;
		case FuId.TextWriterWrite:
			WritePostfix(obj, ".write(");
			EndWrite(args[0]);
			break;
		case FuId.TextWriterWriteChar:
			WriteMethodCall(obj, "write(String.fromCharCode", args[0]);
			WriteChar(')');
			break;
		case FuId.TextWriterWriteCodePoint:
			WriteMethodCall(obj, "write(String.fromCodePoint", args[0]);
			WriteChar(')');
			break;
		case FuId.TextWriterWriteLine:
			if (IsReferenceTo(obj, FuId.ConsoleError))
				WriteWriteLine("console.error", args);
			else if (IsReferenceTo(obj, FuId.ConsoleOut))
				WriteWriteLine("console.log", args);
			else {
				WritePostfix(obj, ".write(");
				if (args.Count != 0) {
					// TODO: coalesce string literals
					args[0].Accept(this, FuPriority.Add);
					Write(" + ");
				}
				Write("\"\\n\")");
			}
			break;
		case FuId.ConsoleReadLine:
			this.ConsoleReadLine = true;
			Write("readLineSync()");
			break;
		case FuId.ConsoleWrite:
			Write("process.stdout.write(");
			EndWrite(args[0]);
			break;
		case FuId.ConsoleWriteLine:
			WriteWriteLine("console.log", args);
			break;
		case FuId.BitConverterInt32BitsToSingle:
		case FuId.BitConverterInt64BitsToDouble:
		case FuId.BitConverterSingleToInt32Bits:
		case FuId.BitConverterDoubleToInt64Bits:
			Write("new ");
			WriteArrayElementType(type);
			Write("Array(new ");
			WriteArrayElementType(method.FirstParameter().Type);
			Write("Array([ ");
			args[0].Accept(this, FuPriority.Argument);
			Write(" ]).buffer)[0]");
			break;
		case FuId.ConvertToBase64String:
			Write("btoa(String.fromCodePoint(...");
			WriteSlice(args[0], args[1], args[2], FuPriority.Primary, "subarray");
			Write("))");
			break;
		case FuId.UTF8GetByteCount:
			Write("new TextEncoder().encode(");
			args[0].Accept(this, FuPriority.Argument);
			Write(").length");
			break;
		case FuId.UTF8GetBytes:
			Write("new TextEncoder().encodeInto(");
			args[0].Accept(this, FuPriority.Argument);
			Write(", ");
			if (args[2].IsLiteralZero())
				args[1].Accept(this, FuPriority.Argument);
			else
				WriteMethodCall(args[1], "subarray", args[2]);
			WriteChar(')');
			break;
		case FuId.UTF8GetString:
			Write("new TextDecoder().decode(");
			WriteSlice(args[0], args[1], args[2], FuPriority.Argument, "subarray");
			WriteChar(')');
			break;
		case FuId.EnvironmentGetEnvironmentVariable:
			if (args[0] is FuLiteralString literal && IsIdentifier(literal.Value)) {
				Write("process.env.");
				Write(literal.Value);
			}
			else {
				Write("process.env[");
				args[0].Accept(this, FuPriority.Argument);
				WriteChar(']');
			}
			break;
		case FuId.DateTimeOffsetUtcNowToUnixTimeMilliseconds:
			Write("BigInt(Date.now())");
			break;
		case FuId.RegexCompile:
			WriteNewRegex(args, 0);
			break;
		case FuId.RegexEscape:
			WriteCall("RegExp.escape", args[0]);
			break;
		case FuId.RegexIsMatchStr:
			WriteNewRegex(args, 1);
			WriteCall(".test", args[0]);
			break;
		case FuId.RegexIsMatchRegex:
			WriteMethodCall(obj, "test", args[0]);
			break;
		case FuId.MatchFindStr:
		case FuId.MatchFindRegex:
			if (parent > FuPriority.Equality)
				WriteChar('(');
			WriteChar('(');
			obj.Accept(this, FuPriority.Assign);
			Write(" = ");
			if (method.Id == FuId.MatchFindStr)
				WriteNewRegex(args, 1);
			else
				args[1].Accept(this, FuPriority.Primary);
			WriteCall(".exec", args[0]);
			Write(") != null");
			if (parent > FuPriority.Equality)
				WriteChar(')');
			break;
		case FuId.MatchGetCapture:
			WriteIndexing(obj, args[0]);
			break;
		case FuId.JsonElementParse:
			WriteCall("JSON.parse", args[0]);
			break;
		case FuId.JsonElementIsObject:
			if (parent > FuPriority.Equality)
				WriteChar('(');
			WritePostfix(obj, "?.constructor == Object");
			if (parent > FuPriority.Equality)
				WriteChar(')');
			break;
		case FuId.JsonElementIsArray:
			WriteCall("Array.isArray", obj);
			break;
		case FuId.JsonElementIsString:
			WriteTypeofEquals(obj, "string", parent);
			break;
		case FuId.JsonElementIsNumber:
			WriteTypeofEquals(obj, "number", parent);
			break;
		case FuId.JsonElementIsBoolean:
			WriteTypeofEquals(obj, "boolean", parent);
			break;
		case FuId.JsonElementIsNull:
			if (parent > FuPriority.Equality)
				WriteChar('(');
			obj.Accept(this, FuPriority.Equality);
			Write(" === null");
			if (parent > FuPriority.Equality)
				WriteChar(')');
			break;
		case FuId.JsonElementGetObject:
		case FuId.JsonElementGetArray:
		case FuId.JsonElementGetString:
		case FuId.JsonElementGetDouble:
		case FuId.JsonElementGetBoolean:
			obj.Accept(this, parent);
			break;
		case FuId.MathAbs:
			WriteCall(args[0].Type.Id == FuId.LongType ? "(x => x < 0n ? -x : x)" : "Math.abs", args[0]);
			break;
		case FuId.MathCeiling:
			WriteCall("Math.ceil", args[0]);
			break;
		case FuId.MathClamp:
			if (HasLong(args)) {
				Write("((x, min, max) => x < min ? min : x > max ? max : x)");
				WriteInParentheses(args);
			}
			else {
				Write("Math.min(Math.max(");
				WriteClampAsMinMax(args);
			}
			break;
		case FuId.MathFusedMultiplyAdd:
			if (parent > FuPriority.Add)
				WriteChar('(');
			args[0].Accept(this, FuPriority.Mul);
			Write(" * ");
			args[1].Accept(this, FuPriority.Mul);
			Write(" + ");
			args[2].Accept(this, FuPriority.Add);
			if (parent > FuPriority.Add)
				WriteChar(')');
			break;
		case FuId.MathIsFinite:
		case FuId.MathIsNaN:
			WriteCamelCase(method.Name);
			WriteInParentheses(args);
			break;
		case FuId.MathIsInfinity:
			if (parent > FuPriority.Equality)
				WriteChar('(');
			WriteCall("Math.abs", args[0]);
			Write(" == Infinity");
			if (parent > FuPriority.Equality)
				WriteChar(')');
			break;
		case FuId.MathMax:
			WriteMathMaxMin(method, "Math.max", '>', args);
			break;
		case FuId.MathMin:
			WriteMathMaxMin(method, "Math.min", '<', args);
			break;
		case FuId.MathTruncate:
			WriteCall("Math.trunc", args[0]);
			break;
		default:
			NotSupported(obj, method.Name);
			break;
		}
	}

	protected override void WriteAssign!(FuBinaryExpr expr, FuPriority parent)
	{
		if (expr.Left is FuBinaryExpr indexing
		 && indexing.Op == FuToken.LeftBracket
		 && indexing.Left.Type is FuClassType dict
		 && dict.Class.Id == FuId.OrderedDictionaryClass)
			WriteMethodCall(indexing.Left, "set", indexing.Right, expr.Right);
		else
			base.WriteAssign(expr, parent);
	}

	protected override void WriteOpAssignRight!(FuBinaryExpr expr)
	{
		WriteCoerced(expr.Left.Type, expr.Right, FuPriority.Argument);
	}

	protected override void WriteIndexingExpr!(FuBinaryExpr expr, FuPriority parent)
	{
		if (expr.Left.Type is FuClassType dict && dict.Class.Id == FuId.OrderedDictionaryClass)
			WriteMethodCall(expr.Left, "get", expr.Right);
		else
			base.WriteIndexingExpr(expr, parent);
	}

	protected override string GetIsOperator() => " instanceof ";

	protected virtual void WriteBoolAndOr!(FuBinaryExpr expr)
	{
		Write("!!");
		base.VisitBinaryExpr(expr, FuPriority.Primary);
	}

	void WriteBoolAndOrAssign!(FuBinaryExpr expr, FuPriority parent)
	{
		expr.Right.Accept(this, parent);
		WriteCharLine(')');
		WriteChar('\t');
		expr.Left.Accept(this, FuPriority.Assign);
	}

	void WriteIsVar!(FuExpr expr, string? name, FuSymbol klass, FuPriority parent)
	{
		if (parent > FuPriority.Rel)
			WriteChar('(');
		if (name != null) {
			WriteChar('(');
			WriteCamelCaseNotKeyword(name);
			Write(" = ");
			expr.Accept(this, FuPriority.Argument);
			WriteChar(')');
		}
		else
			expr.Accept(this, FuPriority.Rel);
		Write(" instanceof ");
		Write(klass.Name);
		if (parent > FuPriority.Rel)
			WriteChar(')');
	}

	internal override void VisitBinaryExpr!(FuBinaryExpr expr, FuPriority parent)
	{
		switch (expr.Op) {
		case FuToken.Slash when expr.Type is FuIntegerType && expr.Type.Id != FuId.LongType:
			if (parent > FuPriority.Or)
				WriteChar('(');
			expr.Left.Accept(this, FuPriority.Mul);
			Write(" / ");
			expr.Right.Accept(this, FuPriority.Primary);
			Write(" | 0");
			if (parent > FuPriority.Or)
				WriteChar(')');
			break;
		case FuToken.And when expr.Type.Id == FuId.BoolType:
		case FuToken.Or when expr.Type.Id == FuId.BoolType:
			WriteBoolAndOr(expr);
			break;
		case FuToken.Xor when expr.Type.Id == FuId.BoolType:
			WriteEqual(expr.Left, expr.Right, parent, true);
			break;
		case FuToken.DivAssign when expr.Type is FuIntegerType && expr.Type.Id != FuId.LongType:
			if (parent > FuPriority.Assign)
				WriteChar('(');
			expr.Left.Accept(this, FuPriority.Assign);
			Write(" = ");
			expr.Left.Accept(this, FuPriority.Mul); // FIXME: side effect
			Write(" / ");
			expr.Right.Accept(this, FuPriority.Primary);
			Write(" | 0");
			if (parent > FuPriority.Assign)
				WriteChar(')');
			break;
		case FuToken.AndAssign when expr.Type.Id == FuId.BoolType:
			Write("if (!"); // FIXME: picks up parent "else"
			WriteBoolAndOrAssign(expr, FuPriority.Primary);
			Write(" = false");
			break;
		case FuToken.OrAssign when expr.Type.Id == FuId.BoolType:
			Write("if ("); // FIXME: picks up parent "else"
			WriteBoolAndOrAssign(expr, FuPriority.Argument);
			Write(" = true");
			break;
		case FuToken.XorAssign when expr.Type.Id == FuId.BoolType:
			expr.Left.Accept(this, FuPriority.Assign);
			Write(" = ");
			WriteEqual(expr.Left, expr.Right, FuPriority.Argument, true); // FIXME: side effect
			break;
		case FuToken.Is when expr.Right is FuVar def:
			WriteIsVar(expr.Left, def.Name, def.Type, parent);
			break;
		default:
			base.VisitBinaryExpr(expr, parent);
			break;
		}
	}

	internal override void VisitLambdaExpr!(FuLambdaExpr expr)
	{
		WriteName(expr.First);
		Write(" => ");
		if (HasTemporaries(expr.Body)) {
			OpenBlock();
			WriteTemporaries(expr.Body);
			Write("return ");
			expr.Body.Accept(this, FuPriority.Argument);
			WriteCharLine(';');
			CloseBlock();
		}
		else
			expr.Body.Accept(this, FuPriority.Statement);
	}

	protected override void StartTemporaryVar!(FuType type)
	{
		assert false;
	}

	protected override void DefineObjectLiteralTemporary!(FuUnaryExpr expr)
	{
	}

	protected virtual void WriteAsType!(FuVar def)
	{
	}

	void WriteVarCast!(FuVar def, FuExpr value)
	{
		Write(def.IsAssigned ? "let " : "const ");
		WriteCamelCaseNotKeyword(def.Name);
		Write(" = ");
		value.Accept(this, FuPriority.Argument);
		WriteAsType(def);
		WriteCharLine(';');
	}

	protected override void WriteAssertCast!(FuBinaryExpr expr)
	{
		assert expr.Right is FuVar def;
		WriteVarCast(def, expr.Left);
	}

	protected override void WriteAssert!(FuAssert statement)
	{
		if (statement.CompletesNormally()) {
			WriteTemporaries(statement.Cond);
			Write("console.assert(");
			statement.Cond.Accept(this, FuPriority.Argument);
			if (statement.Message != null) {
				Write(", ");
				statement.Message.Accept(this, FuPriority.Argument);
			}
		}
		else {
			Write("throw new Error(");
			if (statement.Message != null)
				statement.Message.Accept(this, FuPriority.Argument);
		}
		WriteLine(");");
	}

	protected override void StartBreakGoto!()
	{
		Write("break fuswitch");
	}

	internal override void VisitForeach!(FuForeach statement)
	{
		Write("for (const ");
		assert statement.Collection.Type is FuClassType klass;
		switch (klass.Class.Id) {
		case FuId.StringClass:
		case FuId.ArrayStorageClass:
		case FuId.ListClass:
		case FuId.HashSetClass:
			WriteName(statement.GetVar());
			Write(" of ");
			statement.Collection.Accept(this, FuPriority.Argument);
			break;
		case FuId.SortedSetClass:
			WriteName(statement.GetVar());
			Write(" of ");
			switch (klass.GetElementType()) {
			case FuNumericType number:
				Write("new ");
				WriteArrayElementType(number);
				Write("Array(");
				break;
			case FuEnum:
				Write("new Int32Array(");
				break;
			default:
				Write("Array.from(");
				break;
			}
			statement.Collection.Accept(this, FuPriority.Argument);
			Write(").sort()");
			break;
		case FuId.DictionaryClass:
		case FuId.SortedDictionaryClass:
		case FuId.OrderedDictionaryClass:
			WriteChar('[');
			WriteName(statement.GetVar());
			Write(", ");
			WriteName(statement.GetValueVar());
			Write("] of ");
			if (klass.Class.Id == FuId.OrderedDictionaryClass)
				statement.Collection.Accept(this, FuPriority.Argument);
			else {
				WriteCall("Object.entries", statement.Collection);
				switch (statement.GetVar().Type) {
				case FuStringType:
					if (klass.Class.Id == FuId.SortedDictionaryClass)
						Write(".sort((a, b) => a[0].localeCompare(b[0]))");
					break;
				case FuNumericType:
				case FuEnum:
					Write(".map(e => [+e[0], e[1]])");
					if (klass.Class.Id == FuId.SortedDictionaryClass)
						Write(".sort((a, b) => a[0] - b[0])");
					break;
				default:
					assert false;
				}
			}
			break;
		default:
			assert false;
		}
		WriteChar(')');
		WriteChild(statement.Body);
	}

	internal override void VisitLock!(FuLock statement)
	{
		NotSupported(statement, "'lock'");
	}

	protected override void WriteSwitchCaseCond!(FuSwitch statement, FuExpr value, FuPriority parent)
	{
		switch (value) {
		case FuSymbolReference symbol when symbol.Symbol is FuClass:
			WriteIsVar(statement.Value, null, symbol.Symbol, parent); // FIXME: side effect in every if
			break;
		case FuVar def:
			WriteIsVar(statement.Value, parent == FuPriority.CondAnd ? def.Name : null, def.Type, parent); // FIXME: side effect in every if
			break;
		default:
			base.WriteSwitchCaseCond(statement, value, parent);
			break;
		}
	}

	protected override void WriteIfCaseBody!(List<FuStatement#> body, bool doWhile, FuSwitch statement, FuCase? kase)
	{
		if (kase != null && kase.Values[0] is FuVar caseVar) {
			WriteChar(' ');
			OpenBlock();
			WriteVarCast(caseVar, statement.Value); // FIXME: side effect
			WriteFirstStatements(kase.Body, FuSwitch.LengthWithoutTrailingBreak(kase.Body));
			CloseBlock();
		}
		else
			base.WriteIfCaseBody(body, doWhile, statement, kase);
	}

	internal override void VisitSwitch!(FuSwitch statement)
	{
		if (statement.IsTypeMatching() || statement.HasWhen()) {
			if (statement.Cases.Any(kase => FuSwitch.HasEarlyBreak(kase.Body))
			 || FuSwitch.HasEarlyBreak(statement.DefaultBody)) {
				Write("fuswitch");
				VisitLiteralLong(this.SwitchesWithGoto.Count, FuPriority.Primary);
				this.SwitchesWithGoto.Add(statement);
				Write(": ");
				OpenBlock();
				WriteSwitchAsIfs(statement, false);
				CloseBlock();
			}
			else
				WriteSwitchAsIfs(statement, false);
		}
		else
			base.VisitSwitch(statement);
	}

	protected override void WriteException!()
	{
		Write("Error");
	}

	protected virtual void StartContainerType!(FuContainerType container)
	{
		WriteNewLine();
		WriteDoc(container.Documentation);
	}

	internal override void VisitEnumValue!(FuConst konst, FuConst? previous)
	{
		if (previous != null)
			WriteCharLine(',');
		WriteDoc(konst.Documentation);
		WriteUppercaseWithUnderscores(konst.Name);
		Write(" : ");
		VisitLiteralLong(konst.Value.IntValue(), FuPriority.Argument);
	}

	protected override void WriteEnum!(FuEnum enu)
	{
		StartContainerType(enu);
		Write("const ");
		Write(enu.Name);
		Write(" = ");
		OpenBlock();
		enu.AcceptValues(this);
		WriteNewLine();
		CloseBlock();
	}

	protected override void WriteConst!(FuConst konst)
	{
		if (konst.Visibility != FuVisibility.Private || konst.Type is FuArrayStorageType) {
			WriteNewLine();
			WriteDoc(konst.Documentation);
			Write("static ");
			WriteName(konst);
			Write(" = ");
			konst.Value.Accept(this, FuPriority.Argument);
			WriteCharLine(';');
		}
	}

	protected override void WriteField!(FuField field)
	{
		WriteDoc(field.Documentation);
		base.WriteVar(field);
		WriteCharLine(';');
	}

	protected override void WriteMethod!(FuMethod method)
	{
		if (method.CallType == FuCallType.Abstract)
			return;
		WriteNewLine();
		WriteMethodDoc(method);
		if (method.CallType == FuCallType.Static)
			Write("static ");
		WriteName(method);
		WriteParameters(method, method.Id != FuId.Main);
		WriteBody(method);
	}

	protected void OpenJsClass!(FuClass klass)
	{
		OpenClass(klass, "", " extends ");
		if (klass.Id == FuId.ExceptionClass) {
			Write("name = \"");
			Write(klass.Name);
			WriteLine("\";");
		}
	}

	protected void WriteConstructor!(FuClass klass)
	{
		WriteLine("constructor()");
		OpenBlock();
		if (klass.Parent is FuClass)
			WriteLine("super();");
		WriteConstructorBody(klass);
		CloseBlock();
	}

	protected override void WriteClass!(FuClass klass, FuProgram program)
	{
		if (!WriteBaseClass(klass, program))
			return;
		StartContainerType(klass);
		OpenJsClass(klass);
		if (NeedsConstructor(klass)) {
			if (klass.Constructor != null)
				WriteDoc(klass.Constructor.Documentation);
			WriteConstructor(klass);
		}
		WriteMembers(klass, true);
		CloseBlock();
	}

	void WriteMain!(FuMethod main)
	{
		WriteNewLine();
		if (main.Type.Id == FuId.IntType)
			Write("process.exit(");
		Write(main.Parent.Name);
		Write(".main(");
		if (main.Parameters.Count() == 1)
			Write("process.argv.slice(2)");
		if (main.Type.Id == FuId.IntType)
			WriteChar(')');
		WriteCharLine(')');
	}

	protected virtual void WriteNamedType!(string name)
	{
	}

	protected void WriteLib!(FuProgram program)
	{
		if (this.StringWriter) {
			WriteNewLine();
			WriteLine("class StringWriter");
			OpenBlock();
			WriteLine("#buf = \"\";");
			WriteNewLine();
			Write("write(s");
			WriteNamedType("string");
			WriteChar(')');
			WriteNamedType("void");
			WriteNewLine();
			OpenBlock();
			WriteLine("this.#buf += s;");
			CloseBlock();

			WriteNewLine();
			Write("clear()");
			WriteNamedType("void");
			WriteNewLine();
			OpenBlock();
			WriteLine("this.#buf = \"\";");
			CloseBlock();

			WriteNewLine();
			Write("toString()");
			WriteNamedType("string");
			WriteNewLine();
			OpenBlock();
			WriteLine("return this.#buf;");
			CloseBlock();
			CloseBlock();
		}
		if (this.ConsoleReadLine) {
			WriteNewLine();
			WriteLine("import fs from \"node:fs\";");
			WriteNewLine();
			Write("function readLineSync()");
			WriteNamedType("string");
			WriteNewLine();
			OpenBlock();
			WriteLine("const buf = Buffer.alloc(1);");
			WriteLine("const result = [];");
			WriteLine("while (fs.readSync(0, buf) == 1 && buf[0] != 10)");
			WriteLine("\tresult.push(buf[0]);");
			WriteLine("if (result.length > 0 && result.at(-1) == 13)");
			WriteLine("\tresult.pop();");
			WriteLine("return new TextDecoder().decode(new Uint8Array(result));");
			CloseBlock();
		}
		if (program.Resources.Count > 0) {
			WriteNewLine();
			WriteLine("class Fu");
			OpenBlock();
			foreach ((string name, List<byte> content) in program.Resources) {
				Write("static ");
				WriteResourceName(name);
				WriteLine(" = new Uint8Array([");
				WriteChar('\t');
				WriteBytes(content);
				WriteLine(" ]);");
			}
			WriteNewLine();
			CloseBlock();
		}
		if (program.Main != null)
			WriteMain(program.Main);
	}

	protected virtual void WriteUseStrict!()
	{
		WriteNewLine();
		WriteLine("\"use strict\";");
	}

	public override void WriteProgram!(FuProgram program, string outputFile, string namespace)
	{
		CreateFile(null, outputFile);
		WriteUseStrict();
		WriteTopLevelNatives(program);
		WriteTypes(program);
		WriteLib(program);
		CloseFile();
	}
}

public class GenJs : GenJsNoModule
{
	protected override void StartContainerType!(FuContainerType container)
	{
		base.StartContainerType(container);
		if (container.IsPublic)
			Write("export ");
	}

	protected override void WriteUseStrict!()
	{
	}
}