diff --git a/langserver/java/body-parser3.js b/langserver/java/body-parser3.js index d8c214d..79a0003 100644 --- a/langserver/java/body-parser3.js +++ b/langserver/java/body-parser3.js @@ -13,11 +13,11 @@ const { tokenize, Token } = require('./tokenizer'); const { resolveTypeOrPackage, resolveNextTypeOrPackage } = require('./type-resolver'); const { genericTypeArgs, typeIdent, typeIdentList } = require('./typeident'); const { TokenList } = require("./TokenList"); -const { AnyMethod, AnyType, AnyValue, ArrayLiteral, Label, LiteralNumber, LiteralValue, Local, - MethodDeclarations, ResolvedIdent, Value, } = require("./body-types"); +const { AnyMethod, AnyType, AnyValue, Label, Local, MethodDeclarations, ResolvedIdent, Value, } = require("./body-types"); const { resolveImports, resolveSingleImport } = require('../java/import-resolver'); const { ArrayIndexExpression } = require("./expressiontypes/ArrayIndexExpression"); +const { ArrayValueExpression } = require("./expressiontypes/ArrayValueExpression"); const { BinaryOpExpression } = require("./expressiontypes/BinaryOpExpression"); const { BracketedExpression } = require("./expressiontypes/BracketedExpression"); const { CastExpression } = require("./expressiontypes/CastExpression"); @@ -29,6 +29,12 @@ const { MethodCallExpression } = require("./expressiontypes/MethodCallExpression const { TernaryOpExpression } = require("./expressiontypes/TernaryOpExpression"); const { ThisMemberExpression } = require("./expressiontypes/ThisMemberExpression"); +const { BooleanLiteral } = require('./expressiontypes/literals/Boolean'); +const { CharacterLiteral } = require('./expressiontypes/literals/Character'); +const { NumberLiteral } = require('./expressiontypes/literals/Number'); +const { NullLiteral } = require('./expressiontypes/literals/Null'); +const { StringLiteral } = require('./expressiontypes/literals/String'); + const { AssertStatement } = require("./statementtypes/AssertStatement"); const { Block } = require("./statementtypes/Block"); const { BreakStatement } = require("./statementtypes/BreakStatement"); @@ -1516,13 +1522,13 @@ function rootTerm(tokens, mdecls, scope, imports, typemap) { matches = new ResolvedIdent(tokens.current.value, [], [], [PrimitiveType.fromName(tokens.current.value)]); break; case 'string-literal': - matches = new ResolvedIdent(tokens.current.value, [new LiteralValue(tokens.current.value, typemap.get('java/lang/String'))]); + matches = new ResolvedIdent(tokens.current.value, [new StringLiteral(tokens.current, typemap.get('java/lang/String'))]); break; case 'char-literal': - matches = new ResolvedIdent(tokens.current.value, [new LiteralValue(tokens.current.value, PrimitiveType.map.C)]); + matches = new ResolvedIdent(tokens.current.value, [new CharacterLiteral(tokens.current)]); break; case 'boolean-literal': - matches = new ResolvedIdent(tokens.current.value, [new LiteralValue(tokens.current.value, PrimitiveType.map.Z)]); + matches = new ResolvedIdent(tokens.current.value, [new BooleanLiteral(tokens.current)]); break; case 'object-literal': // this, super or null @@ -1533,11 +1539,11 @@ function rootTerm(tokens, mdecls, scope, imports, typemap) { const supertype = scoped_type.supers.find(s => s.typeKind === 'class') || typemap.get('java/lang/Object'); matches = new ResolvedIdent(tokens.current.value, [new Value(tokens.current.value, supertype)]); } else { - matches = new ResolvedIdent(tokens.current.value, [new LiteralValue(tokens.current.value, new NullType())]); + matches = new ResolvedIdent(tokens.current.value, [new NullLiteral(tokens.current)]); } break; case /number-literal/.test(tokens.current.kind) && tokens.current.kind: - matches = new ResolvedIdent(tokens.current.value, [LiteralNumber.from(tokens.current)]); + matches = new ResolvedIdent(tokens.current.value, [NumberLiteral.from(tokens.current)]); break; case 'inc-operator': let incop = tokens.getIfKind('inc-operator'); @@ -1582,7 +1588,7 @@ function rootTerm(tokens, mdecls, scope, imports, typemap) { tokens.expectValue('}'); } const ident = `{${elements.map(e => e.source).join(',')}}`; - return new ResolvedIdent(ident, [new ArrayLiteral(ident, elements)]); + return new ResolvedIdent(ident, [new ArrayValueExpression(elements)]); default: addproblem(tokens, ParseProblem.Error(tokens.current, 'Expression expected')); return new ResolvedIdent(''); diff --git a/langserver/java/body-types.js b/langserver/java/body-types.js index c04fa60..adb1454 100644 --- a/langserver/java/body-types.js +++ b/langserver/java/body-types.js @@ -1,4 +1,4 @@ -const { JavaType, ArrayType, PrimitiveType, Method, Parameter, Field } = require('java-mti'); +const { JavaType, ArrayType, Method, Parameter, Field } = require('java-mti'); const { Token } = require('./tokenizer'); class ResolvedIdent { @@ -145,22 +145,6 @@ class Value extends ValueBase { this.name = name; this.type = type; } - - /** - * @param {string} ident - * @param {ResolvedIdent} lhs - * @param {ResolvedIdent} rhs - * @param {JavaType} type - */ - static build(ident, lhs, rhs, type) { - if (!lhs.variables[0] || !rhs.variables[0]) { - return new Value(ident, type); - } - if (lhs.variables[0] instanceof LiteralValue && rhs.variables && rhs.variables[0] instanceof LiteralValue) { - new LiteralValue(ident, type); - } - return new Value(ident, type); - } } class AnyValue extends Value { @@ -169,167 +153,6 @@ class AnyValue extends Value { } } -class LiteralValue extends Value { } - -/** - * LiteralNumberType is a value representing literal numbers (like 0, 5.3, -0.1e+12, etc). - * - * It's used to allow literal numbers to be type-assignable to variables with different primitive types. - * For example, 200 is type-assignable to short, int, long, float and double, but not byte. - */ -class LiteralNumber extends LiteralValue { - /** - * @param {string} value - * @param {string} kind - * @param {PrimitiveType} default_type - */ - constructor(value, kind, default_type) { - super(value, default_type); - this.numberValue = value; - this.numberKind = kind; - } - - static shift(a, b, op) { - const ai = a.toInt(), bi = b.toInt(); - if (ai === null || bi === null) { - return null; - } - const val = op(ai, bi); - const type = a.type.typeSignature === 'J' ? PrimitiveType.map.J : PrimitiveType.map.I; - return new LiteralNumber(val.toString(), 'int-number-literal', type); - } - - static bitwise(a, b, op) { - const ai = a.toInt(), bi = b.toInt(); - if (ai === null || bi === null) { - return null; - } - const val = op(ai, bi); - const typekey = a.type.typeSignature+ b.type.typeSignature; - let type = /J/.test(typekey) ? PrimitiveType.map.J : PrimitiveType.map.I; - return new LiteralNumber(val.toString(), 'int-number-literal', type); - } - - static math(a, b, op, divmod) { - const ai = a.toNumber(), bi = b.toNumber(); - if (bi === 0 && divmod) { - return null; - } - let val = op(ai, bi); - const typekey = a.type.typeSignature+ b.type.typeSignature; - if (!/[FD]/.test(typekey) && divmod) { - val = Math.trunc(val); - } - let type; - if (/^(D|F[^D]|J[^FD])/.test(typekey)) { - type = a.type; - } else { - type = b.type; - } - return new LiteralNumber(val.toString(), 'int-number-literal', type); - } - - static '+'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a + b) } - static '-'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a - b) } - static '*'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a * b) } - static '/'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a / b, true) } - static '%'(lhs, rhs) { return LiteralNumber.math(lhs, rhs, (a,b) => a % b, true) } - static '&'(lhs, rhs) { return LiteralNumber.bitwise(lhs, rhs, (a,b) => a & b) } - static '|'(lhs, rhs) { return LiteralNumber.bitwise(lhs, rhs, (a,b) => a | b) } - static '^'(lhs, rhs) { return LiteralNumber.bitwise(lhs, rhs, (a,b) => a ^ b) } - static '>>'(lhs, rhs) { return LiteralNumber.shift(lhs, rhs, (a,b) => a >> b) } - static '>>>'(lhs, rhs) { return LiteralNumber.shift(lhs, rhs, (a,b) => { - // unsigned shift (>>>) is not supported by bigints - // @ts-ignore - return (a >> b) & ~(-1n << (64n - b)); - }) } - static '<<'(lhs, rhs) { return LiteralNumber.shift(lhs, rhs, (a,b) => a << b) } - - toInt() { - switch (this.numberKind) { - case 'hex-number-literal': - case 'int-number-literal': - // unlike parseInt, BigInt doesn't like invalid characters, so - // ensure we strip any trailing long specifier - return BigInt(this.name.match(/(.+?)[lL]?$/)[1]); - } - return null; - } - - toNumber() { - return parseFloat(this.name); - } - - /** - * @param {JavaType} type - */ - isCompatibleWith(type) { - if (this.type === type) { - return true; - } - switch(this.type.simpleTypeName) { - case 'double': - return /^([D]|Ljava\/lang\/(Double);)$/.test(type.typeSignature); - case 'float': - return /^([FD]|Ljava\/lang\/(Float|Double);)$/.test(type.typeSignature); - } - // all integral types are all compatible with long, float and double variables - if (/^([JFD]|Ljava\/lang\/(Long|Float|Double);)$/.test(type.typeSignature)) { - return true; - } - // the desintation type must be a number primitive or one of the corresponding boxed classes - if (!/^([BSIJFDC]|Ljava\/lang\/(Byte|Short|Integer|Long|Float|Double|Character);)$/.test(type.typeSignature)) { - return false; - } - let number = 0; - if (this.numberKind === 'hex-number-literal') { - if (this.numberValue !== '0x') { - const non_leading_zero_digits = this.numberValue.match(/0x0*(.+)/)[1]; - number = non_leading_zero_digits.length > 8 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 16); - } - } else if (this.numberKind === 'int-number-literal') { - const non_leading_zero_digits = this.numberValue.match(/0*(.+)/)[1]; - number = non_leading_zero_digits.length > 10 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 10); - } - if (number >= -128 && number <= 127) { - return true; // byte values are compatible with all other numbers - } - if (number >= -32768 && number <= 32767) { - return !/^([B]|Ljava\/lang\/(Byte);)$/.test(type.typeSignature); // anything except byte - } - return !/^([BSC]|Ljava\/lang\/(Byte|Short|Character);)$/.test(type.typeSignature); // anything except byte, short and character - } - - /** - * @param {Token} token - */ - static from(token) { - function suffix(which) { - switch(which.indexOf(token.value.slice(-1))) { - case 0: - case 1: - return PrimitiveType.map.F; - case 2: - case 3: - return PrimitiveType.map.D; - case 4: - case 5: - return PrimitiveType.map.J; - } - } - switch(token.kind) { - case 'dec-exp-number-literal': - case 'dec-number-literal': - return new LiteralNumber(token.value, token.kind, suffix('FfDdLl') || PrimitiveType.map.D); - case 'hex-number-literal': - return new LiteralNumber(token.value, token.kind, suffix(' Ll') || PrimitiveType.map.I); - case 'int-number-literal': - default: - return new LiteralNumber(token.value, token.kind, suffix('FfDdLl') || PrimitiveType.map.I); - } - } -} - class MethodCall extends Value { /** * @param {string} name @@ -353,17 +176,6 @@ class ConstructorCall extends Value { } } -class ArrayLiteral extends LiteralValue { - /** - * @param {string} name - * @param {ResolvedIdent[]} elements - */ - constructor(name, elements) { - super(name, null); - this.elements = elements; - } -} - class TernaryValue extends Value { /** * @param {string} name @@ -382,11 +194,8 @@ exports.AnyMethod = AnyMethod; exports.AnyType = AnyType; exports.AnyValue = AnyValue; exports.ArrayElement = ArrayElement; -exports.ArrayLiteral = ArrayLiteral; exports.ConstructorCall = ConstructorCall; exports.Label = Label; -exports.LiteralNumber = LiteralNumber; -exports.LiteralValue = LiteralValue; exports.Local = Local; exports.MethodCall = MethodCall; exports.MethodDeclarations = MethodDeclarations; diff --git a/langserver/java/expressiontypes/ArrayValueExpression.js b/langserver/java/expressiontypes/ArrayValueExpression.js new file mode 100644 index 0000000..9c403bd --- /dev/null +++ b/langserver/java/expressiontypes/ArrayValueExpression.js @@ -0,0 +1,16 @@ +/** + * @typedef {import('../body-types').ResolvedIdent} ResolvedIdent + */ +const { Expression } = require("./Expression"); + +class ArrayValueExpression extends Expression { + /** + * @param {ResolvedIdent[]} elements + */ + constructor(elements) { + super(); + this.elements = elements; + } +} + +exports.ArrayValueExpression = ArrayValueExpression; diff --git a/langserver/java/expressiontypes/literals/Boolean.js b/langserver/java/expressiontypes/literals/Boolean.js new file mode 100644 index 0000000..61691a4 --- /dev/null +++ b/langserver/java/expressiontypes/literals/Boolean.js @@ -0,0 +1,18 @@ +/** + * @typedef {import('../../tokenizer').Token} Token + */ +const { LiteralValue } = require('./LiteralValue'); +const { PrimitiveType } = require('java-mti'); + +class BooleanLiteral extends LiteralValue { + /** + * + * @param {Token} token + */ + constructor(token) { + super(token); + this.type = PrimitiveType.map.Z; + } +} + +exports.BooleanLiteral = BooleanLiteral; diff --git a/langserver/java/expressiontypes/literals/Character.js b/langserver/java/expressiontypes/literals/Character.js new file mode 100644 index 0000000..03b4c4c --- /dev/null +++ b/langserver/java/expressiontypes/literals/Character.js @@ -0,0 +1,18 @@ +/** + * @typedef {import('../../tokenizer').Token} Token + */ +const { LiteralValue } = require('./LiteralValue'); +const { PrimitiveType } = require('java-mti'); + +class CharacterLiteral extends LiteralValue { + /** + * + * @param {Token} token + */ + constructor(token) { + super(token); + this.type = PrimitiveType.map.C; + } +} + +exports.CharacterLiteral = CharacterLiteral; diff --git a/langserver/java/expressiontypes/literals/LiteralValue.js b/langserver/java/expressiontypes/literals/LiteralValue.js new file mode 100644 index 0000000..b754f2f --- /dev/null +++ b/langserver/java/expressiontypes/literals/LiteralValue.js @@ -0,0 +1,16 @@ +/** + * @typedef {import('../../tokenizer').Token} Token + */ +const { Expression } = require('../Expression'); + +class LiteralValue extends Expression { + /** + * @param {Token} token + */ + constructor(token) { + super(); + this.token = token; + } +} + +exports.LiteralValue = LiteralValue; diff --git a/langserver/java/expressiontypes/literals/Null.js b/langserver/java/expressiontypes/literals/Null.js new file mode 100644 index 0000000..0f8a79a --- /dev/null +++ b/langserver/java/expressiontypes/literals/Null.js @@ -0,0 +1,18 @@ +/** + * @typedef {import('../../tokenizer').Token} Token + */ +const { LiteralValue } = require('./LiteralValue'); +const { NullType } = require('java-mti'); + +class NullLiteral extends LiteralValue { + /** + * + * @param {Token} token + */ + constructor(token) { + super(token); + this.type = new NullType(); + } +} + +exports.NullLiteral = NullLiteral; diff --git a/langserver/java/expressiontypes/literals/Number.js b/langserver/java/expressiontypes/literals/Number.js new file mode 100644 index 0000000..3eb3938 --- /dev/null +++ b/langserver/java/expressiontypes/literals/Number.js @@ -0,0 +1,167 @@ +/** + * @typedef {import('../../tokenizer').Token} Token + * @typedef {import('java-mti').JavaType} JavaType + */ +const { LiteralValue } = require('./LiteralValue'); +const { PrimitiveType } = require('java-mti'); + +/** + * NumberLiteral is a value representing literal numbers (like 0, 5.3, -0.1e+12, etc). + * + * It allows literal numbers to be type-assignable to variables with different primitive types. + * For example, 200 is type-assignable to short, int, long, float and double, but not byte. + */ +class NumberLiteral extends LiteralValue { + /** + * @param {Token} value + * @param {string} kind + * @param {PrimitiveType} default_type + */ + constructor(value, kind, default_type) { + super(value); + this.numberKind = kind; + this.type = default_type; + } + + static shift(a, b, op) { + const ai = a.toInt(), bi = b.toInt(); + if (ai === null || bi === null) { + return null; + } + const val = op(ai, bi); + const type = a.type.typeSignature === 'J' ? PrimitiveType.map.J : PrimitiveType.map.I; + return new NumberLiteral(val.toString(), 'int-number-literal', type); + } + + static bitwise(a, b, op) { + const ai = a.toInt(), bi = b.toInt(); + if (ai === null || bi === null) { + return null; + } + const val = op(ai, bi); + const typekey = a.type.typeSignature+ b.type.typeSignature; + let type = /J/.test(typekey) ? PrimitiveType.map.J : PrimitiveType.map.I; + return new NumberLiteral(val.toString(), 'int-number-literal', type); + } + + static math(a, b, op, divmod) { + const ai = a.toNumber(), bi = b.toNumber(); + if (bi === 0 && divmod) { + return null; + } + let val = op(ai, bi); + const typekey = a.type.typeSignature+ b.type.typeSignature; + if (!/[FD]/.test(typekey) && divmod) { + val = Math.trunc(val); + } + let type; + if (/^(D|F[^D]|J[^FD])/.test(typekey)) { + type = a.type; + } else { + type = b.type; + } + return new NumberLiteral(val.toString(), 'int-number-literal', type); + } + + static '+'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a + b) } + static '-'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a - b) } + static '*'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a * b) } + static '/'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a / b, true) } + static '%'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a % b, true) } + static '&'(lhs, rhs) { return NumberLiteral.bitwise(lhs, rhs, (a,b) => a & b) } + static '|'(lhs, rhs) { return NumberLiteral.bitwise(lhs, rhs, (a,b) => a | b) } + static '^'(lhs, rhs) { return NumberLiteral.bitwise(lhs, rhs, (a,b) => a ^ b) } + static '>>'(lhs, rhs) { return NumberLiteral.shift(lhs, rhs, (a,b) => a >> b) } + static '>>>'(lhs, rhs) { return NumberLiteral.shift(lhs, rhs, (a,b) => { + // unsigned shift (>>>) is not supported by bigints + // @ts-ignore + return (a >> b) & ~(-1n << (64n - b)); + }) } + static '<<'(lhs, rhs) { return NumberLiteral.shift(lhs, rhs, (a,b) => a << b) } + + toInt() { + switch (this.numberKind) { + case 'hex-number-literal': + case 'int-number-literal': + // unlike parseInt, BigInt doesn't like invalid characters, so + // ensure we strip any trailing long specifier + return BigInt(this.token.value.match(/(.+?)[lL]?$/)[1]); + } + return null; + } + + toNumber() { + return parseFloat(this.token.value); + } + + /** + * @param {JavaType} type + */ + isCompatibleWith(type) { + if (this.type === type) { + return true; + } + switch(this.type.simpleTypeName) { + case 'double': + return /^([D]|Ljava\/lang\/(Double);)$/.test(type.typeSignature); + case 'float': + return /^([FD]|Ljava\/lang\/(Float|Double);)$/.test(type.typeSignature); + } + // all integral types are all compatible with long, float and double variables + if (/^([JFD]|Ljava\/lang\/(Long|Float|Double);)$/.test(type.typeSignature)) { + return true; + } + // the desintation type must be a number primitive or one of the corresponding boxed classes + if (!/^([BSIJFDC]|Ljava\/lang\/(Byte|Short|Integer|Long|Float|Double|Character);)$/.test(type.typeSignature)) { + return false; + } + let number = 0; + if (this.numberKind === 'hex-number-literal') { + if (this.token.value !== '0x') { + const non_leading_zero_digits = this.token.value.match(/0x0*(.+)/)[1]; + number = non_leading_zero_digits.length > 8 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 16); + } + } else if (this.numberKind === 'int-number-literal') { + const non_leading_zero_digits = this.token.value.match(/0*(.+)/)[1]; + number = non_leading_zero_digits.length > 10 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 10); + } + if (number >= -128 && number <= 127) { + return true; // byte values are compatible with all other numbers + } + if (number >= -32768 && number <= 32767) { + return !/^([B]|Ljava\/lang\/(Byte);)$/.test(type.typeSignature); // anything except byte + } + return !/^([BSC]|Ljava\/lang\/(Byte|Short|Character);)$/.test(type.typeSignature); // anything except byte, short and character + } + + /** + * @param {Token} token + */ + static from(token) { + function suffix(which) { + switch(which.indexOf(token.value.slice(-1))) { + case 0: + case 1: + return PrimitiveType.map.F; + case 2: + case 3: + return PrimitiveType.map.D; + case 4: + case 5: + return PrimitiveType.map.J; + } + } + switch(token.kind) { + case 'dec-exp-number-literal': + case 'dec-number-literal': + return new NumberLiteral(token, token.kind, suffix('FfDdLl') || PrimitiveType.map.D); + case 'hex-number-literal': + return new NumberLiteral(token, token.kind, suffix(' Ll') || PrimitiveType.map.I); + case 'int-number-literal': + default: + return new NumberLiteral(token, token.kind, suffix('FfDdLl') || PrimitiveType.map.I); + } + } +} + +exports.NumberLiteral = NumberLiteral; \ No newline at end of file diff --git a/langserver/java/expressiontypes/literals/String.js b/langserver/java/expressiontypes/literals/String.js new file mode 100644 index 0000000..4c25437 --- /dev/null +++ b/langserver/java/expressiontypes/literals/String.js @@ -0,0 +1,19 @@ +/** + * @typedef {import('../../tokenizer').Token} Token + * @typedef {import('java-mti').CEIType} CEIType + */ +const { LiteralValue } = require('./LiteralValue'); + +class StringLiteral extends LiteralValue { + /** + * + * @param {Token} token + * @param {CEIType} string_type + */ + constructor(token, string_type) { + super(token); + this.type = string_type; + } +} + +exports.StringLiteral = StringLiteral;