From 6badc9fdb663e2c6c67ed79a1ecda1fc9291ada0 Mon Sep 17 00:00:00 2001 From: Dave Holoway Date: Sun, 21 Jun 2020 13:47:56 +0100 Subject: [PATCH] implement method body and ststement validation --- langserver/java/TokenList.js | 9 +++ langserver/java/body-parser3.js | 58 +++++++++---------- langserver/java/body-types.js | 10 ++++ langserver/java/expression-resolver.js | 51 ++++++++++------ .../expressiontypes/BinaryOpExpression.js | 57 ++++++++++-------- .../java/expressiontypes/CastExpression.js | 11 +++- .../java/expressiontypes/MemberExpression.js | 10 +++- .../java/expressiontypes/literals/Number.js | 15 +++-- langserver/java/statement-validater.js | 34 +++++++++++ .../java/statementtypes/AssertStatement.js | 29 ++++++++++ langserver/java/statementtypes/Block.js | 26 +++++++++ .../java/statementtypes/BreakStatement.js | 19 ++++++ .../java/statementtypes/ContinueStatement.js | 19 ++++++ langserver/java/statementtypes/DoStatement.js | 19 +++++- .../statementtypes/ExpressionStatement.js | 40 +++++++++++++ .../java/statementtypes/ForStatement.js | 14 +++++ langserver/java/statementtypes/IfStatement.js | 22 +++++++ .../java/statementtypes/LocalDeclStatement.js | 33 +++++++++++ .../java/statementtypes/ReturnStatement.js | 48 +++++++++++++++ langserver/java/statementtypes/Statement.js | 3 + .../java/statementtypes/SwitchStatement.js | 55 ++++++++++++++++++ .../statementtypes/SynchronizedStatement.js | 21 +++++++ .../java/statementtypes/ThrowStatement.js | 21 +++++++ .../java/statementtypes/TryStatement.js | 21 ++++++- .../java/statementtypes/WhileStatement.js | 17 +++++- 25 files changed, 575 insertions(+), 87 deletions(-) create mode 100644 langserver/java/statement-validater.js create mode 100644 langserver/java/statementtypes/ExpressionStatement.js create mode 100644 langserver/java/statementtypes/LocalDeclStatement.js diff --git a/langserver/java/TokenList.js b/langserver/java/TokenList.js index c1f5159..8f63ad6 100644 --- a/langserver/java/TokenList.js +++ b/langserver/java/TokenList.js @@ -18,6 +18,15 @@ class TokenList { this.marks = []; } + /** + * Returns and consumes the current token + */ + consume() { + const tok = this.current; + this.inc(); + return tok; + } + inc() { for (; ;) { this.current = this.tokens[this.idx += 1]; diff --git a/langserver/java/body-parser3.js b/langserver/java/body-parser3.js index 320197c..adc562a 100644 --- a/langserver/java/body-parser3.js +++ b/langserver/java/body-parser3.js @@ -14,9 +14,10 @@ const { resolveTypeOrPackage, resolveNextTypeOrPackage } = require('./type-resol const { genericTypeArgs, typeIdent, typeIdentList } = require('./typeident'); const { TokenList } = require("./TokenList"); const { AnyMethod, AnyType, AnyValue } = require("./anys"); -const { Label, Local, MethodDeclarations, ResolvedIdent } = require("./body-types"); +const { Label, Local, MethodDeclarations, ResolvedIdent, ResolveInfo } = require("./body-types"); const { resolveImports, resolveSingleImport } = require('../java/import-resolver'); const { checkAssignment, getTypeInheritanceList } = require('./expression-resolver'); +const { checkStatementBlock } = require('./statement-validater'); const { ArrayIndexExpression } = require("./expressiontypes/ArrayIndexExpression"); const { ArrayValueExpression } = require("./expressiontypes/ArrayValueExpression"); @@ -46,9 +47,11 @@ const { BreakStatement } = require("./statementtypes/BreakStatement"); const { ContinueStatement } = require("./statementtypes/ContinueStatement"); const { DoStatement } = require("./statementtypes/DoStatement"); const { EmptyStatement } = require("./statementtypes/EmptyStatement"); +const { ExpressionStatement } = require("./statementtypes/ExpressionStatement"); const { ForStatement } = require("./statementtypes/ForStatement"); const { IfStatement } = require("./statementtypes/IfStatement"); const { InvalidStatement } = require("./statementtypes/InvalidStatement"); +const { LocalDeclStatement } = require("./statementtypes/LocalDeclStatement"); const { ReturnStatement } = require("./statementtypes/ReturnStatement"); const { Statement } = require("./statementtypes/Statement"); const { SwitchStatement } = require("./statementtypes/SwitchStatement"); @@ -98,6 +101,7 @@ function parseBody(method, imports, typemap) { let mdecls = new MethodDeclarations(); try { block = statementBlock(tokenlist, mdecls, method, imports, typemap); + checkStatementBlock(block, method, typemap, tokenlist.problems); } catch (err) { addproblem(tokenlist, ParseProblem.Information(tokenlist.current, `Parse failed: ${err.message}`)); @@ -214,8 +218,9 @@ function parse(source, typemap) { timeEnd('parse'); // once all the types have been parsed, resolve any field initialisers + const ri = new ResolveInfo(typemap, tokens.problems); unit.types.forEach(t => { - t.fields.filter(f => f.init).forEach(f => checkAssignment(f.init, f.type, typemap, tokens.problems)); + t.fields.filter(f => f.init).forEach(f => checkAssignment(ri, f.type, f.init)); }); } catch(err) { @@ -380,7 +385,7 @@ function addLocals(tokens, mdecls, new_locals) { * @param {SourceMC} method * @param {ResolvedImport[]} imports * @param {Map} typemap - * @returns {ResolvedIdent|Local[]|Statement} + * @returns {Statement} */ function statement(tokens, mdecls, method, imports, typemap) { let s, modifiers = []; @@ -399,10 +404,10 @@ function statement(tokens, mdecls, method, imports, typemap) { // modifiers are only allowed on local variable decls if (modifiers.length) { const type = typeIdent(tokens, method, imports, typemap); - s = var_ident_list(modifiers, type, null, tokens, mdecls, method, imports, typemap) - addLocals(tokens, mdecls, s); + const locals = var_ident_list(modifiers, type, null, tokens, mdecls, method, imports, typemap) + addLocals(tokens, mdecls, locals); semicolon(tokens); - return s; + return new LocalDeclStatement(locals); } switch(tokens.current.kind) { @@ -422,9 +427,12 @@ function statement(tokens, mdecls, method, imports, typemap) { } // fall-through to expression_or_var_decl case 'primitive-type': - s = expression_or_var_decl(tokens, mdecls, method, imports, typemap); - if (Array.isArray(s)) { - addLocals(tokens, mdecls, s); + const exp_or_vardecl = expression_or_var_decl(tokens, mdecls, method, imports, typemap); + if (Array.isArray(exp_or_vardecl)) { + addLocals(tokens, mdecls, exp_or_vardecl); + s = new LocalDeclStatement(exp_or_vardecl); + } else { + s = new ExpressionStatement(exp_or_vardecl); } semicolon(tokens); return s; @@ -438,7 +446,8 @@ function statement(tokens, mdecls, method, imports, typemap) { case 'unary-operator': case 'open-bracket': case 'new-operator': - s = expression(tokens, mdecls, method, imports, typemap); + const e = expression(tokens, mdecls, method, imports, typemap); + s = new ExpressionStatement(e); semicolon(tokens); return s; } @@ -858,7 +867,7 @@ function statementBlock(tokens, mdecls, method, imports, typemap) { const s = statement(tokens, mdecls, method, imports, typemap); block.statements.push(s); } - mdecls.popScope(); + block.decls = mdecls.popScope(); return block; } @@ -898,21 +907,13 @@ function statementKeyword(tokens, mdecls, method, imports, typemap) { s.statement = statement(tokens, mdecls, method, imports, typemap); break; case 'break': - tokens.inc(); - s = new BreakStatement(); - if (tokens.current.kind === 'ident') { - s.target = tokens.current; - tokens.inc(); - } + s = new BreakStatement(tokens.consume()); + s.target = tokens.getIfKind('ident'); semicolon(tokens); break; case 'continue': - tokens.inc(); - s = new ContinueStatement(); - if (tokens.current.kind === 'ident') { - s.target = tokens.current; - tokens.inc(); - } + s = new ContinueStatement(tokens.consume()); + s.target = tokens.getIfKind('ident'); semicolon(tokens); break; case 'switch': @@ -934,8 +935,8 @@ function statementKeyword(tokens, mdecls, method, imports, typemap) { tryStatement(s, tokens, mdecls, method, imports, typemap); break; case 'return': + s = new ReturnStatement(tokens.current); tokens.inc(); - s = new ReturnStatement(); s.expression = isExpressionStart(tokens.current) ? expression(tokens, mdecls, method, imports, typemap) : null; semicolon(tokens); break; @@ -1234,25 +1235,24 @@ function caseBlock(s, tokens, mdecls, method, imports, typemap) { * @param {Map} typemap */ function caseExpressionList(cases, tokens, mdecls, method, imports, typemap) { - let c = caseExpression(cases, tokens, mdecls, method, imports, typemap); + let c = caseExpression(tokens, mdecls, method, imports, typemap); if (!c) { return; } while (c) { cases.push(c); - c = caseExpression(cases, tokens, mdecls, method, imports, typemap); + c = caseExpression(tokens, mdecls, method, imports, typemap); } } /** -* @param {(ResolvedIdent|boolean)[]} cases * @param {TokenList} tokens * @param {MethodDeclarations} mdecls * @param {SourceMC} method * @param {ResolvedImport[]} imports * @param {Map} typemap */ -function caseExpression(cases, tokens, mdecls, method, imports, typemap) { +function caseExpression(tokens, mdecls, method, imports, typemap) { /** @type {boolean|ResolvedIdent} */ let e = tokens.isValue('default'); if (!e) { @@ -1558,7 +1558,7 @@ function rootTerm(tokens, mdecls, scope, imports, typemap) { return new ResolvedIdent(ident, [new ArrayValueExpression(elements, open)]); default: addproblem(tokens, ParseProblem.Error(tokens.current, 'Expression expected')); - return new ResolvedIdent(''); + return new ResolvedIdent('', [new AnyValue('')]); } tokens.inc(); return matches; diff --git a/langserver/java/body-types.js b/langserver/java/body-types.js index 386b429..c373fd7 100644 --- a/langserver/java/body-types.js +++ b/langserver/java/body-types.js @@ -121,8 +121,18 @@ class ResolveInfo { } } +class ValidateInfo extends ResolveInfo { + constructor(typemap, problems, method) { + super(typemap, problems); + this.method = method; + /** @type {('if'|'else'|'for'|'while'|'do'|'switch'|'try'|'synchronized')[]} */ + this.statementStack = []; + } +} + exports.Label = Label; exports.Local = Local; exports.MethodDeclarations = MethodDeclarations; exports.ResolvedIdent = ResolvedIdent; exports.ResolveInfo = ResolveInfo; +exports.ValidateInfo = ValidateInfo; diff --git a/langserver/java/expression-resolver.js b/langserver/java/expression-resolver.js index 9767418..b7b9061 100644 --- a/langserver/java/expression-resolver.js +++ b/langserver/java/expression-resolver.js @@ -7,28 +7,16 @@ const ParseProblem = require('./parsetypes/parse-problem'); const { TypeVariable, JavaType, PrimitiveType, NullType, ArrayType, CEIType, WildcardType, TypeVariableType, InferredTypeArgument } = require('java-mti'); const { AnyType, ArrayValueType, MultiValueType } = require('./anys'); const { ResolveInfo } = require('./body-types'); -const { LiteralValue } = require('./expressiontypes/literals/LiteralValue'); const { NumberLiteral } = require('./expressiontypes/literals/Number'); -const { Expression } = require('./expressiontypes/Expression'); -const { Variable } = require('./expressiontypes/Variable'); /** - * @param {import('./body-types').ResolvedIdent} e + * @param {ResolveInfo} ri + * @param {ResolvedIdent} expression * @param {JavaType} assign_type - * @param {Map} typemap - * @param {ParseProblem[]} problems */ -function checkAssignment(e, assign_type, typemap, problems) { - const value = e.variables[0]; - if (value instanceof Variable) { - checkTypeAssignable(assign_type, value.type, () => value.name_token, problems); - return; - } - if (value instanceof Expression) { - const expression_result_type = value.resolveExpression(new ResolveInfo(typemap, problems)); - checkTypeAssignable(assign_type, expression_result_type, () => value.tokens(), problems); - return; - } +function checkAssignment(ri, assign_type, expression) { + const value = expression.resolveExpression(ri); + checkTypeAssignable(assign_type, value, () => expression.tokens, ri.problems); } /** @@ -180,9 +168,14 @@ const valid_primitive_types = { /** * Returns true if a value of value_type is assignable to a variable of dest_type * @param {JavaType} dest_type - * @param {JavaType} value_type + * @param {JavaType|NumberLiteral} value_type */ function isTypeAssignable(dest_type, value_type) { + + if (value_type instanceof NumberLiteral) { + return value_type.isCompatibleWith(dest_type); + } + let is_assignable = false; if (dest_type.typeSignature === value_type.typeSignature) { // exact signature match @@ -255,6 +248,23 @@ function isTypeArgumentCompatible(dest_typevar, value_typevar_type) { return isTypeAssignable(dest_typevar.type, value_typevar_type); } +/** + * + * @param {ResolvedValue} value + * @param {() => Token[]} tokens + * @param {ParseProblem[]} problems + */ +function checkBooleanBranchCondition(value, tokens, problems) { + if (value instanceof JavaType) { + if (!isTypeAssignable(PrimitiveType.map.Z, value)) { + problems.push(ParseProblem.Error(tokens(), `Boolean expression expected, but type '${value.fullyDottedTypeName}' found.`)); + } + return; + } + problems.push(ParseProblem.Error(tokens(), `Boolean expression expected.`)); +} + + /** * @param {CEIType} type */ @@ -276,6 +286,9 @@ function getTypeInheritanceList(type) { return Array.from(types.done); } -exports.checkAssignment = checkAssignment; exports.checkArrayIndex = checkArrayIndex; +exports.checkAssignment = checkAssignment; +exports.checkBooleanBranchCondition = checkBooleanBranchCondition; +exports.checkTypeAssignable = checkTypeAssignable; exports.getTypeInheritanceList = getTypeInheritanceList; +exports.isTypeAssignable = isTypeAssignable; diff --git a/langserver/java/expressiontypes/BinaryOpExpression.js b/langserver/java/expressiontypes/BinaryOpExpression.js index 59ecc26..9ae7e36 100644 --- a/langserver/java/expressiontypes/BinaryOpExpression.js +++ b/langserver/java/expressiontypes/BinaryOpExpression.js @@ -8,6 +8,7 @@ const { JavaType, PrimitiveType } = require('java-mti'); const ParseProblem = require('../parsetypes/parse-problem'); const { AnyType, TypeIdentType } = require('../anys'); const { NumberLiteral } = require('./literals/Number'); +const { checkTypeAssignable } = require('../expression-resolver'); class BinaryOpExpression extends Expression { /** @@ -27,46 +28,43 @@ class BinaryOpExpression extends Expression { */ resolveExpression(ri) { const operator = this.op.value; - let lhstype = this.lhs.resolveExpression(ri); - let rhstype = this.rhs.resolveExpression(ri); + const lhsvalue = this.lhs.resolveExpression(ri); + const rhsvalue = this.rhs.resolveExpression(ri); - if (lhstype instanceof AnyType || rhstype instanceof AnyType) { + if (lhsvalue instanceof AnyType || rhsvalue instanceof AnyType) { return AnyType.Instance; } - if (lhstype instanceof NumberLiteral || rhstype instanceof NumberLiteral) { - if (lhstype instanceof NumberLiteral && rhstype instanceof NumberLiteral) { + if (lhsvalue instanceof NumberLiteral || rhsvalue instanceof NumberLiteral) { + if (lhsvalue instanceof NumberLiteral && rhsvalue instanceof NumberLiteral) { // if they are both literals, compute the result if (/^[*/%+-]$/.test(operator)) { - return NumberLiteral[operator](lhstype, rhstype); + return NumberLiteral[operator](lhsvalue, rhsvalue); } - if (/^([&|^]|<<|>>>?)$/.test(operator) && !/[FD]/.test(`${lhstype.type.typeSignature}${rhstype.type.typeSignature}`)) { - return NumberLiteral[operator](lhstype, rhstype); + if (/^([&|^]|<<|>>>?)$/.test(operator) && !/[FD]/.test(`${lhsvalue.type.typeSignature}${rhsvalue.type.typeSignature}`)) { + return NumberLiteral[operator](lhsvalue, rhsvalue); } } - if (lhstype instanceof NumberLiteral) { - lhstype = lhstype.type; - } - if (rhstype instanceof NumberLiteral) { - rhstype = rhstype.type; - } } + const lhstype = lhsvalue instanceof JavaType ? lhsvalue : lhsvalue instanceof NumberLiteral ? lhsvalue.type : null; + const rhstype = rhsvalue instanceof JavaType ? rhsvalue : rhsvalue instanceof NumberLiteral ? rhsvalue.type : null; + if (operator === 'instanceof') { - if (!(rhstype instanceof TypeIdentType)) { + if (!(rhsvalue instanceof TypeIdentType)) { ri.problems.push(ParseProblem.Error(this.rhs.tokens, `Type expected`)); } - if (!(lhstype instanceof JavaType)) { + if (!lhstype) { ri.problems.push(ParseProblem.Error(this.rhs.tokens, `Expression expected`)); } return PrimitiveType.map.Z; } - if (!(lhstype instanceof JavaType) || !(rhstype instanceof JavaType)) { - if (!(lhstype instanceof JavaType)) { + if (!lhstype || !rhstype) { + if (!lhstype) { ri.problems.push(ParseProblem.Error(this.lhs.tokens, `Expression expected`)); } - if (!(rhstype instanceof JavaType)) { + if (!rhstype) { ri.problems.push(ParseProblem.Error(this.rhs.tokens, `Expression expected`)); } return AnyType.Instance; @@ -74,13 +72,17 @@ class BinaryOpExpression extends Expression { const typekey = `${lhstype.typeSignature}#${rhstype.typeSignature}`; - if (operator === '+' && typekey.startsWith('Ljava/lang/String;')) { + if (operator === '+' && /(^|#)Ljava\/lang\/String;/.test(typekey)) { // string appending is compatible with all types - return lhstype; + return ri.typemap.get('java/lang/String'); } if (/^([*/%&|^+-]?=|<<=|>>>?=)$/.test(operator)) { - checkOperator(operator.slice(0,-1), ri, this.op, typekey, lhstype, rhstype); + let src_type = rhsvalue; + if (operator.length > 1) { + src_type = checkOperator(operator.slice(0,-1), ri, this.op, typekey, lhstype, rhstype); + } + checkTypeAssignable(lhstype, src_type, () => this.rhs.tokens, ri.problems); // result of assignments are lhs return lhstype; } @@ -104,9 +106,14 @@ class BinaryOpExpression extends Expression { */ function checkOperator(operator, ri, operator_token, typekey, lhstype, rhstype) { + if (operator === '+' && /(^|#)Ljava\/lang\/String;/.test(typekey)) { + // string appending is compatible with all types + return ri.typemap.get('java/lang/String'); + } + if (/^[*/%+-]$/.test(operator)) { // math operators - must be numeric - if (!/^[BSIJFD]#[BSIJFD]$/.test(typekey)) { + if (!/^[BSIJFDC]#[BSIJFDC]$/.test(typekey)) { ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`)); } if (/^(D|F#[^D]|J#[^FD]|I#[^JFD])/.test(typekey)) { @@ -120,7 +127,7 @@ function checkOperator(operator, ri, operator_token, typekey, lhstype, rhstype) if (/^(<<|>>>?)$/.test(operator)) { // shift operators - must be integral - if (!/^[BSIJ]#[BSIJ]$/.test(typekey)) { + if (!/^[BSIJC]#[BSIJC]$/.test(typekey)) { ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`)); } if (/^J/.test(typekey)) { @@ -131,7 +138,7 @@ function checkOperator(operator, ri, operator_token, typekey, lhstype, rhstype) if (/^[&|^]$/.test(operator)) { // bitwise or logical operators - if (!/^[BSIJ]#[BSIJ]$|^Z#Z$/.test(typekey)) { + if (!/^[BSIJC]#[BSIJC]$|^Z#Z$/.test(typekey)) { ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`)); } if (/^[JZ]/.test(typekey)) { diff --git a/langserver/java/expressiontypes/CastExpression.js b/langserver/java/expressiontypes/CastExpression.js index 7ace94c..53d6785 100644 --- a/langserver/java/expressiontypes/CastExpression.js +++ b/langserver/java/expressiontypes/CastExpression.js @@ -4,10 +4,11 @@ * @typedef {import('../anys').ResolvedValue} ResolvedValue */ const { Expression } = require("./Expression"); -const { AnyType, TypeIdentType } = require('../anys'); +const { AnyType, MultiValueType, TypeIdentType } = require('../anys'); const ParseProblem = require('../parsetypes/parse-problem'); const { JavaType, PrimitiveType, NullType, CEIType, ArrayType } = require('java-mti'); const { getTypeInheritanceList } = require('../expression-resolver'); +const { NumberLiteral } = require('../expressiontypes/literals/Number'); class CastExpression extends Expression { /** @@ -55,6 +56,14 @@ function checkCastable(cast, cast_type, expr_type, problems) { } return; } + if (expr_type instanceof NumberLiteral) { + checkCastable(cast, cast_type, expr_type.type, problems); + return; + } + if (expr_type instanceof MultiValueType) { + expr_type.types.forEach(type => checkCastable(cast, cast_type, type, problems)); + return; + } problems.push(ParseProblem.Error(cast.expression.tokens, `Invalid cast: expression is not a value or variable`)); } diff --git a/langserver/java/expressiontypes/MemberExpression.js b/langserver/java/expressiontypes/MemberExpression.js index 7869101..5c7076f 100644 --- a/langserver/java/expressiontypes/MemberExpression.js +++ b/langserver/java/expressiontypes/MemberExpression.js @@ -4,7 +4,7 @@ * @typedef {import('../tokenizer').Token} Token */ const { Expression } = require("./Expression"); -const { CEIType } = require('java-mti'); +const { JavaType, CEIType } = require('java-mti'); const { AnyType, MethodType, PackageNameType, TypeIdentType } = require('../anys'); const { getTypeInheritanceList } = require('../expression-resolver'); const { resolveNextPackage } = require('../type-resolver'); @@ -46,7 +46,7 @@ class MemberExpression extends Expression { : AnyType.Instance; } - if (!(instance instanceof CEIType)) { + if (!(instance instanceof JavaType)) { ri.problems.push(ParseProblem.Error(this.member, `Unresolved member: '${ident}'`)); return AnyType.Instance; } @@ -54,6 +54,12 @@ class MemberExpression extends Expression { if (field) { return field.type; } + + if (!(instance instanceof CEIType)) { + ri.problems.push(ParseProblem.Error(this.member, `Unresolved member: '${ident}'`)); + return AnyType.Instance; + } + let methods = new Map(); getTypeInheritanceList(instance).forEach(type => { type.methods.forEach(m => { diff --git a/langserver/java/expressiontypes/literals/Number.js b/langserver/java/expressiontypes/literals/Number.js index bfa4fb8..f935e77 100644 --- a/langserver/java/expressiontypes/literals/Number.js +++ b/langserver/java/expressiontypes/literals/Number.js @@ -81,30 +81,29 @@ class NumberLiteral extends LiteralValue { * @param {NumberLiteral} a * @param {NumberLiteral} b * @param {(a,b) => Number} op - * @param {boolean} [divmod] */ - static math(a, b, op, divmod) { + static math(a, b, op) { 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) { + if (!/[FD]/.test(typekey)) { val = Math.trunc(val); } const type = typekey.includes('D') ? PrimitiveType.map.D : typekey.includes('F') ? PrimitiveType.map.F : typekey.includes('J') ? PrimitiveType.map.J : PrimitiveType.map.I; + // note: Java allows integer division by zero at compile-time - it will + // always cause an ArithmeticException at runtime, so the result here (inf or nan) + // is largely meaningless return NumberLiteral.calc(a, b, 'int-number-literal', type, val); } 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.math(lhs, rhs, (a,b) => a / b) } + static '%'(lhs, rhs) { return NumberLiteral.math(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.bitwise(lhs, rhs, (a,b) => a ^ b) } diff --git a/langserver/java/statement-validater.js b/langserver/java/statement-validater.js new file mode 100644 index 0000000..774080c --- /dev/null +++ b/langserver/java/statement-validater.js @@ -0,0 +1,34 @@ +const ParseProblem = require('./parsetypes/parse-problem'); + +const { CEIType } = require('java-mti') +const { SourceMethod, SourceConstructor, SourceInitialiser } = require('./source-types'); + +const { Block } = require("./statementtypes/Block"); +const { Statement } = require("./statementtypes/Statement"); +const { LocalDeclStatement } = require("./statementtypes/LocalDeclStatement"); + +const { ValidateInfo } = require('./body-types'); + +/** + * @param {Block} block + * @param {SourceMethod | SourceConstructor | SourceInitialiser} method + * @param {Map} typemap + * @param {ParseProblem[]} problems + */ +function checkStatementBlock(block, method, typemap, problems) { + block.validate(new ValidateInfo(typemap, problems, method)); +} + +/** + * @param {Statement} statement + * @param {ValidateInfo} vi + */ +function checkNonVarDeclStatement(statement, vi) { + if (statement instanceof LocalDeclStatement) { + vi.problems.push(ParseProblem.Error(statement.locals[0].decltoken, `Local variables cannot be declared as single conditional statements`)); + }; + statement.validate(vi); +} + +exports.checkStatementBlock = checkStatementBlock; +exports.checkNonVarDeclStatement = checkNonVarDeclStatement; diff --git a/langserver/java/statementtypes/AssertStatement.js b/langserver/java/statementtypes/AssertStatement.js index 72d65f4..0fdf439 100644 --- a/langserver/java/statementtypes/AssertStatement.js +++ b/langserver/java/statementtypes/AssertStatement.js @@ -1,8 +1,37 @@ +/** + * @typedef {import('../tokenizer').Token} Token + * @typedef {import('../body-types').ResolvedIdent} ResolvedIdent + * @typedef {import('../body-types').ValidateInfo} ValidateInfo + */ const { Statement } = require("./Statement"); +const ParseProblem = require('../parsetypes/parse-problem'); +const { isTypeAssignable } = require('../expression-resolver'); +const { JavaType, PrimitiveType } = require('java-mti'); class AssertStatement extends Statement { + /** @type {ResolvedIdent} */ expression = null; + /** @type {ResolvedIdent} */ message = null; + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + if (this.expression) { + const value = this.expression.resolveExpression(vi); + if (!(value instanceof JavaType) || !isTypeAssignable(PrimitiveType.map.Z, value)) { + vi.problems.push(ParseProblem.Error(this.expression.tokens, `Boolean expression expected`)); + } + } + + if (this.message) { + const msg_value = this.message.resolveExpression(vi); + if (!(msg_value instanceof JavaType) || !isTypeAssignable(vi.typemap.get('java/lang/String'), msg_value)) { + vi.problems.push(ParseProblem.Error(this.message.tokens, `String expression expected`)); + } + } + } } exports.AssertStatement = AssertStatement; diff --git a/langserver/java/statementtypes/Block.js b/langserver/java/statementtypes/Block.js index fd3cfe8..4323b8b 100644 --- a/langserver/java/statementtypes/Block.js +++ b/langserver/java/statementtypes/Block.js @@ -1,12 +1,21 @@ /** * @typedef {import('../tokenizer').Token} Token + * @typedef {import('../body-types').Local} Local + * @typedef {import('../body-types').Label} Label + * @typedef {import('../body-types').ValidateInfo} ValidateInfo + * @typedef {import('../source-types').SourceType} SourceType */ const { Statement } = require("./Statement"); +const ParseProblem = require('../parsetypes/parse-problem'); +const { checkAssignment } = require('../expression-resolver'); class Block extends Statement { /** @type {Statement[]} */ statements = []; + /** @type {{locals: Local[], labels: Label[], types: SourceType[]}} */ + decls = null; + /** * @param {Token} open */ @@ -14,6 +23,23 @@ class Block extends Statement { super(); this.open = open; } + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + if (this.decls) { + const locals = this.decls.locals.reverse(); + locals.forEach(local => { + if (locals.find(l => l.name === local.name) !== local) { + vi.problems.push(ParseProblem.Error(local.decltoken, `Variable redeclared: ${local.name}`)) + } + }); + } + for (let statement of this.statements) { + statement.validate(vi); + } + } } exports.Block = Block; diff --git a/langserver/java/statementtypes/BreakStatement.js b/langserver/java/statementtypes/BreakStatement.js index 98867c1..b290c52 100644 --- a/langserver/java/statementtypes/BreakStatement.js +++ b/langserver/java/statementtypes/BreakStatement.js @@ -1,11 +1,30 @@ /** * @typedef {import('../tokenizer').Token} Token + * @typedef {import('../body-types').ValidateInfo} ValidateInfo */ const { Statement } = require("./Statement"); +const ParseProblem = require('../parsetypes/parse-problem'); class BreakStatement extends Statement { /** @type {Token} */ target = null; + + /** + * @param {Token} token + */ + constructor(token) { + super(); + this.break_token = token; + } + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + if (!vi.statementStack.find(s => /^(for|do|while|switch)$/.test(s))) { + vi.problems.push(ParseProblem.Error(this.break_token, `break can only be specified inside loop/switch statements`)); + } + } } exports.BreakStatement = BreakStatement; diff --git a/langserver/java/statementtypes/ContinueStatement.js b/langserver/java/statementtypes/ContinueStatement.js index 466ea42..f6e2af0 100644 --- a/langserver/java/statementtypes/ContinueStatement.js +++ b/langserver/java/statementtypes/ContinueStatement.js @@ -1,11 +1,30 @@ /** * @typedef {import('../tokenizer').Token} Token + * @typedef {import('../body-types').ValidateInfo} ValidateInfo */ const { Statement } = require("./Statement"); +const ParseProblem = require('../parsetypes/parse-problem'); class ContinueStatement extends Statement { /** @type {Token} */ target = null; + + /** + * @param {Token} token + */ + constructor(token) { + super(); + this.continue_token = token; + } + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + if (!vi.statementStack.find(s => /^(for|do|while)$/.test(s))) { + vi.problems.push(ParseProblem.Error(this.continue_token, `continue can only be specified inside loop statements`)); + } + } } exports.ContinueStatement = ContinueStatement; diff --git a/langserver/java/statementtypes/DoStatement.js b/langserver/java/statementtypes/DoStatement.js index 15a1dd3..72fafc4 100644 --- a/langserver/java/statementtypes/DoStatement.js +++ b/langserver/java/statementtypes/DoStatement.js @@ -1,14 +1,31 @@ /** + * @typedef {import('../tokenizer').Token} Token * @typedef {import('../body-types').ResolvedIdent} ResolvedIdent - * @typedef {import('./Block').Block} Block + * @typedef {import('../body-types').ValidateInfo} ValidateInfo + * @typedef {import('../expressiontypes/Expression').Expression} Expression + * @typedef {import('../statementtypes/Block').Block} Block */ const { Statement } = require("./Statement"); +const { checkBooleanBranchCondition } = require('../expression-resolver'); class DoStatement extends Statement { /** @type {ResolvedIdent} */ test = null; /** @type {Block} */ block = null; + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + if (this.block) { + vi.statementStack.unshift('do'); + this.block.validate(vi); + vi.statementStack.shift(); + } + const value = this.test.resolveExpression(vi); + checkBooleanBranchCondition(value, () => this.test.tokens, vi.problems); + } } exports.DoStatement = DoStatement; diff --git a/langserver/java/statementtypes/ExpressionStatement.js b/langserver/java/statementtypes/ExpressionStatement.js new file mode 100644 index 0000000..5c813e8 --- /dev/null +++ b/langserver/java/statementtypes/ExpressionStatement.js @@ -0,0 +1,40 @@ +/** + * @typedef {import('../tokenizer').Token} Token + * @typedef {import('../body-types').ResolvedIdent} ResolvedIdent + * @typedef {import('../body-types').ValidateInfo} ValidateInfo + * @typedef {import('../expressiontypes/Expression').Expression} Expression + */ +const { Statement } = require("./Statement"); +const { BinaryOpExpression } = require('../expressiontypes/BinaryOpExpression'); +const { MethodCallExpression } = require('../expressiontypes/MethodCallExpression'); +const { NewObject } = require('../expressiontypes/NewExpression'); +const { IncDecExpression } = require('../expressiontypes/IncDecExpression'); +const ParseProblem = require('../parsetypes/parse-problem'); + +class ExpressionStatement extends Statement { + /** + * @param {ResolvedIdent} expression + */ + constructor(expression) { + super(); + this.expression = expression; + } + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + // only method calls, new objects, increments and assignments are allowed as expression statements + const e = this.expression.variables[0]; + let is_statement = e instanceof MethodCallExpression || e instanceof NewObject || e instanceof IncDecExpression; + if (e instanceof BinaryOpExpression) { + is_statement = e.op.kind === 'assignment-operator'; + } + if (!is_statement) { + vi.problems.push(ParseProblem.Error(this.expression.tokens, `Statement expected`)); + } + this.expression.resolveExpression(vi); + } +} + +exports.ExpressionStatement = ExpressionStatement; diff --git a/langserver/java/statementtypes/ForStatement.js b/langserver/java/statementtypes/ForStatement.js index ccaca4b..9abf684 100644 --- a/langserver/java/statementtypes/ForStatement.js +++ b/langserver/java/statementtypes/ForStatement.js @@ -1,9 +1,11 @@ /** * @typedef {import('../body-types').Local} Local * @typedef {import('../body-types').ResolvedIdent} ResolvedIdent + * @typedef {import('../body-types').ValidateInfo} ValidateInfo * @typedef {import('../tokenizer').Token} Token */ const { Statement } = require("./Statement"); +const { checkNonVarDeclStatement } = require('../statement-validater'); class ForStatement extends Statement { /** @type {ResolvedIdent[] | Local[]} */ @@ -16,6 +18,18 @@ class ForStatement extends Statement { iterable = null; /** @type {Statement} */ statement = null; + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + + if (this.statement) { + vi.statementStack.unshift('for'); + checkNonVarDeclStatement(this.statement, vi); + vi.statementStack.shift(); + } + } } exports.ForStatement = ForStatement; diff --git a/langserver/java/statementtypes/IfStatement.js b/langserver/java/statementtypes/IfStatement.js index 37d27cc..6c9addc 100644 --- a/langserver/java/statementtypes/IfStatement.js +++ b/langserver/java/statementtypes/IfStatement.js @@ -1,7 +1,10 @@ /** * @typedef {import('../body-types').ResolvedIdent} ResolvedIdent + * @typedef {import('../body-types').ValidateInfo} ValidateInfo */ const { Statement } = require("./Statement"); +const { checkBooleanBranchCondition } = require('../expression-resolver'); +const { checkNonVarDeclStatement } = require('../statement-validater'); class IfStatement extends Statement { /** @type {ResolvedIdent} */ @@ -10,6 +13,25 @@ class IfStatement extends Statement { statement = null; /** @type {Statement} */ elseStatement = null; + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + const value = this.test.resolveExpression(vi); + checkBooleanBranchCondition(value, () => this.test.tokens, vi.problems); + + if (this.statement) { + vi.statementStack.unshift('if'); + checkNonVarDeclStatement(this.statement, vi); + vi.statementStack.shift(); + } + if (this.elseStatement) { + vi.statementStack.unshift('else'); + checkNonVarDeclStatement(this.statement, vi); + vi.statementStack.shift(); + } + } } exports.IfStatement = IfStatement; diff --git a/langserver/java/statementtypes/LocalDeclStatement.js b/langserver/java/statementtypes/LocalDeclStatement.js new file mode 100644 index 0000000..db88c6a --- /dev/null +++ b/langserver/java/statementtypes/LocalDeclStatement.js @@ -0,0 +1,33 @@ +/** + * @typedef {import('../tokenizer').Token} Token + * @typedef {import('../body-types').Local} Local + * @typedef {import('../body-types').Label} Label + * @typedef {import('../body-types').ValidateInfo} ValidateInfo + * @typedef {import('../source-types').SourceType} SourceType + */ +const { Statement } = require("./Statement"); +const ParseProblem = require('../parsetypes/parse-problem'); +const { checkAssignment } = require('../expression-resolver'); + +class LocalDeclStatement extends Statement { + /** + * @param {Local[]} locals + */ + constructor(locals) { + super(); + this.locals = locals; + } + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + this.locals.forEach(local => { + if (local.init) { + checkAssignment(vi, local.type, local.init); + } + }); + } +} + +exports.LocalDeclStatement = LocalDeclStatement; diff --git a/langserver/java/statementtypes/ReturnStatement.js b/langserver/java/statementtypes/ReturnStatement.js index 32666e1..72a4939 100644 --- a/langserver/java/statementtypes/ReturnStatement.js +++ b/langserver/java/statementtypes/ReturnStatement.js @@ -1,11 +1,59 @@ /** * @typedef {import('../body-types').ResolvedIdent} ResolvedIdent + * @typedef {import('../body-types').ValidateInfo} ValidateInfo + * @typedef {import('../tokenizer').Token} Token */ +const { JavaType, PrimitiveType } = require('java-mti'); const { Statement } = require("./Statement"); +const ParseProblem = require('../parsetypes/parse-problem'); +const { isTypeAssignable } = require('../expression-resolver'); +const { NumberLiteral } = require('../expressiontypes/literals/Number'); +const { MultiValueType } = require('../anys'); class ReturnStatement extends Statement { /** @type {ResolvedIdent} */ expression = null; + + /** + * @param {Token} return_token + */ + constructor(return_token) { + super(); + this.return_token = return_token; + } + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + const method_return_type = vi.method.returnType; + if (!this.expression) { + if (method_return_type !== PrimitiveType.map.V) { + vi.problems.push(ParseProblem.Error(this.return_token, `Method must return a value of type '${method_return_type.fullyDottedTypeName}'`)); + } + return; + } + if (method_return_type === PrimitiveType.map.V) { + vi.problems.push(ParseProblem.Error(this.expression.tokens, `void method cannot return a value`)); + return; + } + const type = this.expression.resolveExpression(vi); + checkType(type); + + function checkType(type) { + if (type instanceof JavaType || type instanceof NumberLiteral) { + if (!isTypeAssignable(method_return_type, type)) { + const expr_type = type instanceof NumberLiteral ? type.type : type; + vi.problems.push(ParseProblem.Error(this.expression.tokens, `Incompatible types: expression of type '${expr_type.fullyDottedTypeName}' cannot be returned from a method of type '${method_return_type.fullyDottedTypeName}'`)); + } + } else if (type instanceof MultiValueType) { + // ternary, eg. return x > 0 ? 1 : 2; + type.types.forEach(type => checkType(type)); + } else { + vi.problems.push(ParseProblem.Error(this.expression.tokens, `'${method_return_type.fullyDottedTypeName}' type expression expected`)); + } + } + } } exports.ReturnStatement = ReturnStatement; diff --git a/langserver/java/statementtypes/Statement.js b/langserver/java/statementtypes/Statement.js index cffe6cf..2fee098 100644 --- a/langserver/java/statementtypes/Statement.js +++ b/langserver/java/statementtypes/Statement.js @@ -1,4 +1,7 @@ class Statement { + + validate(vi) {} + } exports.Statement = Statement; diff --git a/langserver/java/statementtypes/SwitchStatement.js b/langserver/java/statementtypes/SwitchStatement.js index 5d2f58b..73c0b1d 100644 --- a/langserver/java/statementtypes/SwitchStatement.js +++ b/langserver/java/statementtypes/SwitchStatement.js @@ -1,13 +1,68 @@ /** * @typedef {import('../body-types').ResolvedIdent} ResolvedIdent + * @typedef {import('../body-types').ValidateInfo} ValidateInfo + * @typedef {import('../tokenizer').Token} Token */ +const { JavaType, PrimitiveType } = require('java-mti'); const { Statement } = require("./Statement"); +const ParseProblem = require('../parsetypes/parse-problem'); +const { isTypeAssignable } = require('../expression-resolver'); +const { NumberLiteral } = require('../expressiontypes/literals/Number'); class SwitchStatement extends Statement { /** @type {ResolvedIdent} */ test = null; + /** @type {(ResolvedIdent|boolean)[]} */ cases = []; + /** @type {{cases: (ResolvedIdent|boolean)[], statements: Statement[]} []} */ caseBlocks = []; + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + let test_type = null; + if (this.test) { + test_type = this.test.resolveExpression(vi); + if (test_type instanceof NumberLiteral) { + test_type = test_type.type; + } + if (test_type instanceof JavaType) { + if (!isTypeAssignable(vi.typemap.get('java/lang/String'), test_type)) { + if (!isTypeAssignable(PrimitiveType.map.I, test_type)) { + test_type = null; + } + } + } else { + test_type = null; + } + if (!test_type) { + vi.problems.push(ParseProblem.Error(this.test.tokens, `Switch expression must be of type 'int' or 'java.lang.String'`)); + } + } + + vi.statementStack.unshift('switch'); + + this.caseBlocks.forEach(caseblock => { + caseblock.cases.forEach(c => { + if (typeof c === 'boolean') { + // default case + return; + } + const case_value = c.resolveExpression(vi); + if (case_value instanceof JavaType || case_value instanceof NumberLiteral) { + if (test_type && !isTypeAssignable(test_type, case_value)) { + const case_type = case_value instanceof JavaType ? case_value : case_value.type; + vi.problems.push(ParseProblem.Error(c.tokens, `Incomparable types: expression of type '${case_type.fullyDottedTypeName}' is not comparable with type '${test_type.fullyDottedTypeName}'`)); + } + } else { + vi.problems.push(ParseProblem.Error(c.tokens, `Expression expected`)); + } + }) + }) + + vi.statementStack.shift(); + } } exports.SwitchStatement = SwitchStatement; diff --git a/langserver/java/statementtypes/SynchronizedStatement.js b/langserver/java/statementtypes/SynchronizedStatement.js index 1316373..746765d 100644 --- a/langserver/java/statementtypes/SynchronizedStatement.js +++ b/langserver/java/statementtypes/SynchronizedStatement.js @@ -1,13 +1,34 @@ /** * @typedef {import('../body-types').ResolvedIdent} ResolvedIdent + * @typedef {import('../body-types').ValidateInfo} ValidateInfo */ +const { CEIType } = require('java-mti'); const { Statement } = require("./Statement"); +const ParseProblem = require('../parsetypes/parse-problem'); class SynchronizedStatement extends Statement { /** @type {ResolvedIdent} */ expression = null; /** @type {Statement} */ statement = null; + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + if (this.expression) { + const value = this.expression.resolveExpression(vi); + // locks must be a reference type + if (!(value instanceof CEIType)) { + vi.problems.push(ParseProblem.Error(this.expression.tokens, `Lock expression must be a reference type`)); + } + } + if (this.statement) { + vi.statementStack.unshift('synchronized'); + this.statement.validate(vi); + vi.statementStack.shift(); + } + } } exports.SynchronizedStatement = SynchronizedStatement; diff --git a/langserver/java/statementtypes/ThrowStatement.js b/langserver/java/statementtypes/ThrowStatement.js index 70a8e93..dbe9939 100644 --- a/langserver/java/statementtypes/ThrowStatement.js +++ b/langserver/java/statementtypes/ThrowStatement.js @@ -1,11 +1,32 @@ /** * @typedef {import('../body-types').ResolvedIdent} ResolvedIdent + * @typedef {import('../body-types').ValidateInfo} ValidateInfo */ +const { JavaType } = require('java-mti'); const { Statement } = require("./Statement"); +const { isTypeAssignable } = require('../expression-resolver'); +const ParseProblem = require('../parsetypes/parse-problem'); class ThrowStatement extends Statement { /** @type {ResolvedIdent} */ expression = null; + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + if (!this.expression) { + return; + } + const throw_value = this.expression.resolveExpression(vi); + if (throw_value instanceof JavaType) { + if (!isTypeAssignable(vi.typemap.get('java/lang/Throwable'), throw_value)) { + vi.problems.push(ParseProblem.Error(this.expression.tokens, `throw expression does not inherit from java.lang.Throwable`)); + } + } else { + vi.problems.push(ParseProblem.Error(this.expression.tokens, `Throwable expression expected`)); + } + } } exports.ThrowStatement = ThrowStatement; diff --git a/langserver/java/statementtypes/TryStatement.js b/langserver/java/statementtypes/TryStatement.js index f9c14f1..86d9418 100644 --- a/langserver/java/statementtypes/TryStatement.js +++ b/langserver/java/statementtypes/TryStatement.js @@ -1,9 +1,11 @@ /** - * @typedef {import('../body-types').ResolvedIdent} ResolvedIdent + * @typedef {import('../body-types').ValidateInfo} ValidateInfo * @typedef {import('./Block').Block} Block * @typedef {import('../body-types').Local} Local */ const { Statement } = require("./Statement"); +const { ResolvedIdent } = require('../body-types'); +const ParseProblem = require('../parsetypes/parse-problem'); class TryStatement extends Statement { /** @type {(ResolvedIdent|Local[])[]} */ @@ -11,6 +13,23 @@ class TryStatement extends Statement { /** @type {Block} */ block = null; catches = []; + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + this.resources.forEach(r => { + if (r instanceof ResolvedIdent) { + r.resolveExpression(vi); + } + }); + + if (this.block) { + vi.statementStack.unshift('try'); + this.block.validate(vi); + vi.statementStack.shift(); + } + } } exports.TryStatement = TryStatement; diff --git a/langserver/java/statementtypes/WhileStatement.js b/langserver/java/statementtypes/WhileStatement.js index 3a754b1..dd833db 100644 --- a/langserver/java/statementtypes/WhileStatement.js +++ b/langserver/java/statementtypes/WhileStatement.js @@ -1,14 +1,29 @@ /** * @typedef {import('../body-types').ResolvedIdent} ResolvedIdent - * @typedef {import('./Block').Block} Block + * @typedef {import('../body-types').ValidateInfo} ValidateInfo */ const { Statement } = require("./Statement"); +const { checkBooleanBranchCondition } = require('../expression-resolver'); class WhileStatement extends Statement { /** @type {ResolvedIdent} */ test = null; /** @type {Statement} */ statement = null; + + /** + * @param {ValidateInfo} vi + */ + validate(vi) { + const value = this.test.resolveExpression(vi); + checkBooleanBranchCondition(value, () => this.test.tokens, vi.problems); + + if (this.statement) { + vi.statementStack.unshift('while'); + this.statement.validate(vi); + vi.statementStack.shift(); + } + } } exports.WhileStatement = WhileStatement;