// 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!()
{
}
}